# List account links Source: https://docs.inboxapp.com/api-reference/account-links/list-account-links GET /account-links List all account links for the team # Add leads Source: https://docs.inboxapp.com/api-reference/campaigns/add-leads post /campaigns/{campaignId}/leads Add leads to a campaign by platform IDs, usernames, external IDs, or a mixed leads array. Profiles not yet in the system will be fetched automatically. # Cancel import job Source: https://docs.inboxapp.com/api-reference/campaigns/cancel-import-job post /campaigns/{campaignId}/import-jobs/{jobId}/cancel Cancel a running or queued import job. # Cancel lead Source: https://docs.inboxapp.com/api-reference/campaigns/cancel-lead post /campaigns/{campaignId}/leads/{identifier}/cancel Cancel a lead's campaign sequence, preventing further messages from being sent. The lead transitions to the 'canceled' stage. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # Create campaign Source: https://docs.inboxapp.com/api-reference/campaigns/create-campaign post /campaigns Create a new campaign. Optionally pass rules to configure lead sources and exclusions at creation time, or update them later via [Update lead rules](#tag/campaigns/PATCH/campaigns/{campaignId}/rules). # Create column Source: https://docs.inboxapp.com/api-reference/campaigns/create-column post /campaigns/{campaignId}/columns Create a new custom column on a campaign. Columns can be used as variables in message templates and to store per-lead data. # Create import job Source: https://docs.inboxapp.com/api-reference/campaigns/create-import-job post /campaigns/{campaignId}/import-jobs Create a new import job to add leads to the campaign from various sources. The job runs asynchronously in the background — poll the job status endpoint to track progress. # Delete campaign Source: https://docs.inboxapp.com/api-reference/campaigns/delete-campaign delete /campaigns/{campaignId} Permanently delete a campaign and all associated data including leads, sequences, and automations # Delete column Source: https://docs.inboxapp.com/api-reference/campaigns/delete-column delete /campaigns/{campaignId}/columns/{columnId} Permanently delete a custom column and purge all lead data for this column. This action cannot be undone. # Disable campaign Source: https://docs.inboxapp.com/api-reference/campaigns/disable-campaign post /campaigns/{campaignId}/disable Disable message dispatch for a campaign. This transitions the campaign to paused status. Messages in flight may still be delivered. To re-enable, use [Enable campaign](#tag/campaigns/POST/campaigns/{campaignId}/enable). # Enable campaign Source: https://docs.inboxapp.com/api-reference/campaigns/enable-campaign post /campaigns/{campaignId}/enable Enable message dispatch for a campaign. If the campaign is in draft status, this transitions it to active. If paused, this resumes sending according to the configured schedule. The campaign must have at least one lead, one linked account, and a valid sequence. See [Update sequence](#tag/campaigns/PUT/campaigns/{campaignId}/sequence) and [Add leads](#tag/campaigns/POST/campaigns/{campaignId}/leads). # Get campaign Source: https://docs.inboxapp.com/api-reference/campaigns/get-campaign get /campaigns/{campaignId} Retrieve full details of a specific campaign including its rules, sequence, and automations # Get import job Source: https://docs.inboxapp.com/api-reference/campaigns/get-import-job get /campaigns/{campaignId}/import-jobs/{jobId} Get the current status and progress of an import job. Poll this endpoint to track completion. # Get lead Source: https://docs.inboxapp.com/api-reference/campaigns/get-lead get /campaigns/{campaignId}/leads/{identifier} Get a specific lead from a campaign. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # List campaigns Source: https://docs.inboxapp.com/api-reference/campaigns/list-campaigns get /campaigns List campaigns with optional status filtering and cursor-based pagination # List columns Source: https://docs.inboxapp.com/api-reference/campaigns/list-columns get /campaigns/{campaignId}/columns List all custom columns defined on a campaign. Columns store per-lead data like company name or role, and can be used as variables in message templates. # List import jobs Source: https://docs.inboxapp.com/api-reference/campaigns/list-import-jobs get /campaigns/{campaignId}/import-jobs List import jobs for a campaign with cursor-based pagination. Sorted by creation date, newest first. # List leads Source: https://docs.inboxapp.com/api-reference/campaigns/list-leads get /campaigns/{campaignId}/leads List campaign leads with cursor-based pagination, optional filters, search, and sorting. Returns full prospect data including CRM context, custom column values, and campaign stage. # Pause import job Source: https://docs.inboxapp.com/api-reference/campaigns/pause-import-job post /campaigns/{campaignId}/import-jobs/{jobId}/pause Pause a running import job. It can be resumed later. # Remove leads Source: https://docs.inboxapp.com/api-reference/campaigns/remove-leads delete /campaigns/{campaignId}/leads Remove leads from a campaign by platform IDs, usernames, external IDs, or a mixed leads array. # Resume import job Source: https://docs.inboxapp.com/api-reference/campaigns/resume-import-job post /campaigns/{campaignId}/import-jobs/{jobId}/resume Resume a paused import job. If all import slots are occupied, the job will be queued until a slot becomes available. # Set automations Source: https://docs.inboxapp.com/api-reference/campaigns/set-automations put /campaigns/{campaignId}/automations Set the full list of automations for a campaign. Replaces all existing automations. Automations trigger actions when certain events occur (e.g., setting a status or applying tags when a prospect replies). # Update campaign Source: https://docs.inboxapp.com/api-reference/campaigns/update-campaign patch /campaigns/{campaignId} Update any combination of campaign fields. Only provided fields are changed; omitted fields remain unchanged. # Update column Source: https://docs.inboxapp.com/api-reference/campaigns/update-column patch /campaigns/{campaignId}/columns/{columnId} Update a custom column's name or description. # Update lead Source: https://docs.inboxapp.com/api-reference/campaigns/update-lead patch /campaigns/{campaignId}/leads/{identifier} Update custom column data for a lead in the campaign. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # Update lead rules Source: https://docs.inboxapp.com/api-reference/campaigns/update-lead-rules patch /campaigns/{campaignId}/rules Update the lead rules for a campaign. Lead rules determine which leads are automatically included or excluded. Synced lists continuously feed leads into the campaign. When a lead is added to a synced list, it is added to the campaign. When a lead is removed from all synced lists that contributed it, it is also removed from the campaign. Excluded lists prevent leads from being targeted. Leads in an excluded list are removed from the campaign even if they exist in a synced list. Omitted fields are left unchanged. Pass an empty array to clear synced or excluded lists. # Update sequence Source: https://docs.inboxapp.com/api-reference/campaigns/update-sequence put /campaigns/{campaignId}/sequence Replace the entire sequence with a new set of steps. Creates a new version; previous versions are preserved for historical reference. The server assigns step IDs and ranks from array order. Message steps can use variables — see [Create column](#tag/campaigns/POST/campaigns/{campaignId}/columns) to set up custom variables. # List colors Source: https://docs.inboxapp.com/api-reference/colors/list-colors GET /colors Get a list of predefined colors that can be used when creating or updating tags or statuses # List events Source: https://docs.inboxapp.com/api-reference/events/list-events get /events List webhook events for replay. Events are stored for 7 days. # Bulk cancel import jobs Source: https://docs.inboxapp.com/api-reference/import-jobs/bulk-cancel-import-jobs post /lists/{listId}/import-jobs/bulk-cancel Cancel import jobs. When jobIds is omitted, cancels all non-terminated jobs on the list. Frees up slots for queued jobs. # Bulk pause import jobs Source: https://docs.inboxapp.com/api-reference/import-jobs/bulk-pause-import-jobs post /lists/{listId}/import-jobs/bulk-pause Pause import jobs. When jobIds is omitted, pauses all running and queued jobs on the list. Also freezes the import queue. # Bulk resume import jobs Source: https://docs.inboxapp.com/api-reference/import-jobs/bulk-resume-import-jobs post /lists/{listId}/import-jobs/bulk-resume Resume paused import jobs. When jobIds is omitted, resumes all paused jobs. Jobs are started immediately if slots are available, otherwise queued. Also unfreezes the import queue. # Cancel import job Source: https://docs.inboxapp.com/api-reference/import-jobs/cancel-import-job post /lists/{listId}/import-jobs/{jobId}/cancel Cancel a running or queued import job # Create import job Source: https://docs.inboxapp.com/api-reference/import-jobs/create-import-job post /lists/{listId}/import-jobs Create a new import job to add leads to a list from various sources. Poll the job status endpoint to track progress. # Get import job Source: https://docs.inboxapp.com/api-reference/import-jobs/get-import-job get /lists/{listId}/import-jobs/{jobId} Get the current status and progress of an import job. Poll this endpoint to track completion. # Get import jobs Source: https://docs.inboxapp.com/api-reference/import-jobs/get-import-jobs get /lists/{listId}/import-jobs Get import jobs for a list with cursor-based pagination. Sorted by creation date, newest first. # Pause import job Source: https://docs.inboxapp.com/api-reference/import-jobs/pause-import-job post /lists/{listId}/import-jobs/{jobId}/pause Pause a running import job. It can be resumed later. # Resume import job Source: https://docs.inboxapp.com/api-reference/import-jobs/resume-import-job post /lists/{listId}/import-jobs/{jobId}/resume Resume a paused import job. If all import slots are occupied, the job will be queued until a slot becomes available. # Introduction Source: https://docs.inboxapp.com/api-reference/introduction Welcome to the Inboxapp API documentation ## Welcome to the Inboxapp API The Inboxapp API gives you full access to manage the following entities in your team: * **Threads**: DM conversations on X * **Account links**: X accounts connected to your team * **Members**: users who can access the team * **Prospects**: external profiles on X * **Tags**: labels for prospects * **Statuses**: pipeline statuses for prospects As of now, access to the following is not available: * **Lists**: Saved collections of leads * **Campaigns**: Automated DM sequences ## Authentication All API endpoints are authenticated using Bearer tokens. Include your token in the `Authorization` header: ```bash theme={null} Authorization: Bearer YOUR_API_TOKEN ``` ## Base URL All API requests should be made to: ``` https://inboxapp.com/api/v1 ``` ## A quick note on IDs Inboxapp stores two types of IDs for objects that relate to X: * `id` — The Inbox ID, which is the ID of the Inbox object * `platformId` — The original X 'platform' ID, which is the ID of the X user, conversation, etc. Throughout the API, you'll see instances where multiple IDs are present. `platformId` is always the original X ID. ## Encoding objects and arrays in query params Inboxapp's API uses bracket notation to denote objects and arrays. ### Objects For an endpoint that accepts a `cursor` parameter for instance, with the cursor object being: ```json theme={null} { "id": "l44e15irdq4db30i77cgphhx", "timestamp": "2025-11-10T18:03:16.000Z" } ``` The URL search params would be encoded as: ``` ?cursor[id]=l44e15irdq4db30i77cgphhx&cursor[timestamp]=2025-11-10T18:03:16.000Z ``` For pagination cursors specifically, prefer the flat `cursorId` and `cursorTimestamp` query parameters over bracket notation. Both are supported, but the flat form is simpler to work with. ### Arrays For an endpoint that accepts an `ids` parameter for instance, with the value being: ```json theme={null} ["1", "2", "3"] ``` there are three options: 1. Repeat the parameter for each value: ``` ?ids=1&ids=2&ids=3 ``` 2. Append `[]` to explicitly denote an array (useful if there's only one value): ``` ?ids[]=1&ids[]=2&ids[]=3 ``` 3. Append `[number]` to denote the array index. This option is the most flexible and allows for reordering and skipping values: ``` ?ids[0]=1&ids[1]=2&ids[2]=3 ``` # Add leads Source: https://docs.inboxapp.com/api-reference/leads/add-leads post /lists/{listId}/leads Add leads to a list by platform IDs, usernames, external IDs, or a mixed leads array. Profiles not yet in the system will be fetched automatically. # Get lead Source: https://docs.inboxapp.com/api-reference/leads/get-lead get /lists/{listId}/leads/{identifier} Get a specific lead from a list. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # Get leads Source: https://docs.inboxapp.com/api-reference/leads/get-leads get /lists/{listId}/leads Get leads from a list with cursor-based pagination, optional filters, and sorting. Returns full prospect data including CRM context and custom column values. # Remove leads Source: https://docs.inboxapp.com/api-reference/leads/remove-leads delete /lists/{listId}/leads Remove leads from a list by platform IDs, usernames, external IDs, or a mixed leads array. # Update lead Source: https://docs.inboxapp.com/api-reference/leads/update-lead patch /lists/{listId}/leads/{identifier} Update custom column data for a lead in a list. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # Create column Source: https://docs.inboxapp.com/api-reference/list-columns/create-column post /lists/{listId}/columns Create a new custom column on a list # Delete column Source: https://docs.inboxapp.com/api-reference/list-columns/delete-column delete /lists/{listId}/columns/{columnId} Permanently delete a custom column and purge all lead data for this column # List columns Source: https://docs.inboxapp.com/api-reference/list-columns/list-columns get /lists/{listId}/columns List all custom columns defined on a list # Update column Source: https://docs.inboxapp.com/api-reference/list-columns/update-column patch /lists/{listId}/columns/{columnId} Update a custom column's name or description # Create list Source: https://docs.inboxapp.com/api-reference/lists/create-list post /lists Create a new list for organizing leads # Delete list Source: https://docs.inboxapp.com/api-reference/lists/delete-list delete /lists/{listId} Permanently delete a list and remove all lead associations # Get list Source: https://docs.inboxapp.com/api-reference/lists/get-list get /lists/{listId} Get details of a specific list # Get lists Source: https://docs.inboxapp.com/api-reference/lists/get-lists get /lists Get lists in the team with cursor-based pagination. Optionally filter by list type. # Update list Source: https://docs.inboxapp.com/api-reference/lists/update-list patch /lists/{listId} Update a list's name, icon, color, or description # Get member Source: https://docs.inboxapp.com/api-reference/members/get-member GET /members/{memberId} Get details of a specific member # List members Source: https://docs.inboxapp.com/api-reference/members/list-members GET /members List all members of the team # Add reaction Source: https://docs.inboxapp.com/api-reference/messages/add-reaction post /threads/{threadId}/messages/{messageId}/reactions/reactions Add a reaction to a message. Only works for non-sandbox threads. # Delete message Source: https://docs.inboxapp.com/api-reference/messages/delete-message delete /threads/{threadId}/messages/{messageId} Delete a message in a thread. Only works for X Chat (encrypted DM) threads. # Edit message Source: https://docs.inboxapp.com/api-reference/messages/edit-message patch /threads/{threadId}/messages/{messageId} Edit a message in a thread. Only works for X Chat (encrypted DM) threads. # Get message history Source: https://docs.inboxapp.com/api-reference/messages/get-message-history get /threads/{threadId}/messages/{messageId}/history Get the edit history and deletion status of a message. Returns all versions of the message content over time. # List messages Source: https://docs.inboxapp.com/api-reference/messages/list-messages GET /threads/{threadId}/messages List messages in a thread with pagination support # Quick send message Source: https://docs.inboxapp.com/api-reference/messages/quick-send-message POST /threads/messages Send a message without knowing the thread ID ahead of time. Automatically creates a thread if needed. Sending to a prospect who hasn't messaged you first (outbound) requires the **Outbound Messages addon** (\$199/mo). Replying to existing conversations works on all plans. See [Working with messages](/guides/messages) for details. # Remove reaction Source: https://docs.inboxapp.com/api-reference/messages/remove-reaction delete /threads/{threadId}/messages/{messageId}/reactions/reactions Remove a reaction from a message. Only works for non-sandbox threads. # Send message Source: https://docs.inboxapp.com/api-reference/messages/send-message POST /threads/{threadId}/messages Send a message in a thread If this is the first message in the thread (outbound), the **Outbound Messages addon** (\$199/mo) is required. Replies to existing conversations work on all plans. See [Working with messages](/guides/messages) for details. # Get prospect Source: https://docs.inboxapp.com/api-reference/prospects/get-prospect GET /prospects/{prospectId} Get details of a specific prospect # Lookup prospect Source: https://docs.inboxapp.com/api-reference/prospects/lookup-prospect GET /prospects/lookup Find an Inboxapp prospect by platform ID, username, or external ID. By default the identifier is treated as a platform ID. Use the `by` query parameter to look up by username or external ID instead. # Update prospect context Source: https://docs.inboxapp.com/api-reference/prospects/update-prospect-context PATCH /prospects/{prospectId}/context Update context of a specific prospect # Create status Source: https://docs.inboxapp.com/api-reference/statuses/create-status POST /statuses Create a new status to track leads through your sales pipeline # Delete status Source: https://docs.inboxapp.com/api-reference/statuses/delete-status DELETE /statuses/{statusId} Delete a status. This will remove the status from all threads. # Get status Source: https://docs.inboxapp.com/api-reference/statuses/get-status GET /statuses/{statusId} Get details of a specific status # List available colors Source: https://docs.inboxapp.com/api-reference/statuses/list-available-colors get /statuses/colors Get a list of predefined colors that can be used when creating or updating statuses # List status colors Source: https://docs.inboxapp.com/api-reference/statuses/list-status-colors GET /statuses/colors Get a list of predefined colors that can be used when creating or updating statuses # List statuses Source: https://docs.inboxapp.com/api-reference/statuses/list-statuses GET /statuses List all statuses in the team # Update status Source: https://docs.inboxapp.com/api-reference/statuses/update-status PATCH /statuses/{statusId} Update a status's name or color # Create tag Source: https://docs.inboxapp.com/api-reference/tags/create-tag POST /tags Create a new tag for organizing threads and prospects # Delete tag Source: https://docs.inboxapp.com/api-reference/tags/delete-tag DELETE /tags/{tagId} Delete a tag. This will remove the tag from all threads and prospects. # Get tag Source: https://docs.inboxapp.com/api-reference/tags/get-tag GET /tags/{tagId} Get details of a specific tag # List available colors Source: https://docs.inboxapp.com/api-reference/tags/list-available-colors get /tags/colors Get a list of predefined colors that can be used when creating or updating tags # List tag colors Source: https://docs.inboxapp.com/api-reference/tags/list-tag-colors GET /tags/colors Get a list of predefined colors that can be used when creating or updating tags # List tags Source: https://docs.inboxapp.com/api-reference/tags/list-tags GET /tags List all tags in the team # Update tag Source: https://docs.inboxapp.com/api-reference/tags/update-tag PATCH /tags/{tagId} Update a tag's name or color # Get team Source: https://docs.inboxapp.com/api-reference/teams/get-team GET /team Get the team associated with the supplied API token. # Create thread Source: https://docs.inboxapp.com/api-reference/threads/create-thread POST /threads Create a new active thread with a prospect # Delete thread Source: https://docs.inboxapp.com/api-reference/threads/delete-thread DELETE /threads/{threadId} Soft delete a thread # Get thread Source: https://docs.inboxapp.com/api-reference/threads/get-thread GET /threads/{threadId} Get details of a specific thread # List threads Source: https://docs.inboxapp.com/api-reference/threads/list-threads GET /threads List threads ordered by recency with advanced filtering # Lookup by username Source: https://docs.inboxapp.com/api-reference/threads/lookup-by-username GET /threads/lookup-by-username Find all threads for a prospect by their exact platform username. Returns threads across all account links. # Lookup thread Source: https://docs.inboxapp.com/api-reference/threads/lookup-thread GET /threads/lookup Find a thread by account link ID and external identifier (platform ID or external ID) # Lookup threads by username Source: https://docs.inboxapp.com/api-reference/threads/lookup-threads-by-username get /threads/lookup-by-username Find all threads for a prospect by their exact platform username. Returns threads across all account links. # Send typing indicator Source: https://docs.inboxapp.com/api-reference/threads/send-typing-indicator post /threads/{threadId}/typing Sends a typing indicator to the external party in the thread. Only supported for X Chat (encrypted) threads. # Update thread Source: https://docs.inboxapp.com/api-reference/threads/update-thread PATCH /threads/{threadId} Mark a thread done or change assignee # Update thread settings Source: https://docs.inboxapp.com/api-reference/threads/update-thread-settings patch /threads/{threadId}/settings Update settings for a specific thread # Account links Source: https://docs.inboxapp.com/configuration/account-links Managing connected X accounts in Inboxapp ## Overview Account links represent X (Twitter) accounts connected to your Inboxapp team. Each account link enables you to send and receive DMs through that account. ## List account links Retrieve all connected accounts: ```typescript account-links.ts theme={null} const { data: accountLinks } = await client.get("/account-links"); console.log(`${accountLinks.length} connected account(s)`); accountLinks.forEach((account) => { console.log(`@${account.username} (${account.id})`); }); ``` ```bash cURL theme={null} curl https://inboxapp.com/api/v1/account-links \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} [ { "id": "df6jbw4h36qm5d9iu2sgn7kx", "platform": "twitter", "platformId": "1566123362161725440", "name": "Acme Corp", "username": "acmecorp", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "createdAt": "2024-06-01T12:00:00.000Z", "status": "active", "syncedAt": "2025-01-15T10:30:00.000Z" }, { "id": "g7ht2kw9xr5m8fp3yjnbqvce", "platform": "twitter", "platformId": "1876543210987654321", "name": "Acme Support", "username": "acmesupport", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "createdAt": "2024-08-01T09:00:00.000Z", "status": "active", "syncedAt": "2025-01-14T08:00:00.000Z" } ] ``` `GET /account-links` returns a flat array, not a wrapped object. The same applies to `GET /members`, `GET /tags`, and `GET /statuses`. ### Key fields | Field | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `id` | Inbox account link ID — use this for all API operations | | `platform` | Always `"twitter"` for now | | `platformId` | The X numeric user ID | | `username` | X handle without `@` | | `image` | Profile picture URL | | `status` | `"active"`, `"offline"` (X signed out your account), or `"paused"` (team scheduled for deletion or other rare circumstances) | | `syncedAt` | When this account completed its first full sync. `null` if sync hasn't finished yet | ## Using account links ### Send messages from specific accounts Specify which account to send from using `accountLinkId`: ```typescript theme={null} const { data: accountLinks } = await client.get("/account-links"); const salesAccount = accountLinks.find((a: any) => a.username === "acmecorp"); await client.post("/threads/messages", { externalPlatformId: "1876543210987654321", accountLinkId: salesAccount.id, content: "Hi! Reaching out from our sales team.", }); ``` ### Filter threads by account View conversations for specific accounts using `accountLinkIds`: ```typescript theme={null} const { data } = await client.get("/threads", { params: { "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx", }, }); console.log(`${data.threads.length} threads for this account`); ``` ### Lookup threads by account Check if a thread exists for a specific account–prospect pair: ```typescript theme={null} const { data: thread } = await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", }, }); if (thread) { console.log("Thread exists:", thread.id); } else { console.log("No existing thread"); } ``` ## Multi-account patterns ### Account selection logic Choose the appropriate account based on context: ```typescript theme={null} interface AccountSelector { accountLinkId: string; condition: (prospect: any) => boolean; } const accountRules: AccountSelector[] = [ { accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", condition: (prospect) => prospect.followerCount >= 100000, }, { accountLinkId: "g7ht2kw9xr5m8fp3yjnbqvce", condition: () => true, }, ]; function selectAccount(prospect: any): string { for (const rule of accountRules) { if (rule.condition(prospect)) return rule.accountLinkId; } return accountRules[accountRules.length - 1].accountLinkId; } ``` ### Cross-account thread check Check if you have any existing thread with a prospect across all accounts: ```typescript theme={null} async function findAnyThread(prospectPlatformId: string) { const { data: accountLinks } = await client.get("/account-links"); for (const account of accountLinks) { const { data: thread } = await client.get("/threads/lookup", { params: { externalPlatformId: prospectPlatformId, accountLinkId: account.id, }, }); if (thread) { return { thread, accountLink: account }; } } return null; } const existing = await findAnyThread("1876543210987654321"); if (existing) { console.log(`Found thread via @${existing.accountLink.username}`); } ``` ## ID types Account links have two ID types. Use the correct one: | Property | Example | Use for | | ------------------- | ---------------------------- | ------------------ | | `id` (Inbox ID) | `"df6jbw4h36qm5d9iu2sgn7kx"` | All API operations | | `platformId` (X ID) | `"1566123362161725440"` | Reference only | ```typescript theme={null} // Correct: Use Inbox ID await client.get("/threads", { params: { "accountLinkIds[0]": account.id }, }); // Wrong: Using platform ID await client.get("/threads", { params: { "accountLinkIds[0]": account.platformId }, }); ``` ## Connecting new accounts Account links are connected through the Inboxapp dashboard, not the API. To add a new X account: 1. Go to [inboxapp.com](https://inboxapp.com) 2. Navigate to **Settings → Accounts** 3. Make sure you have the [Chrome extension installed](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe) 4. Click **Open X** and select the account to link The new account will then appear in API responses. ## Related endpoints GET /account-links Filter by accountLinkIds Find thread by account + prospect Send from specific account ## Next steps Configure team members Work with conversations Send and receive messages Send your first message # Authentication Source: https://docs.inboxapp.com/configuration/authentication Obtaining and using your Inboxapp API token ## Get your API token 1. Log in to your Inboxapp dashboard at [inboxapp.com](https://inboxapp.com) 2. Navigate to **Settings → API** 3. Click **New API token** 4. Enter a name for your token and click **Create** 5. Copy the token immediately — you won't see it again Store your API token securely. Never commit it to version control or share it publicly. ## Using the token Include your token in the `Authorization` header of every request: ``` Authorization: Bearer YOUR_API_TOKEN ``` ### Environment variable setup ```bash theme={null} # Add to your shell profile or .env file export INBOX_API_TOKEN="your_token_here" ``` ### Making requests ```typescript axios theme={null} import axios from "axios"; const client = axios.create({ baseURL: "https://inboxapp.com/api/v1", headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); const { data: team } = await client.get("/team"); console.log("Connected to:", team.name); ``` ```javascript fetch theme={null} const response = await fetch("https://inboxapp.com/api/v1/team", { headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); const team = await response.json(); console.log("Connected to:", team.name); ``` ```bash cURL theme={null} curl https://inboxapp.com/api/v1/team \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" ``` **Expected response:** ```json theme={null} { "id": "hzcai5t59nn9vsck3rbuepyg", "name": "Acme Corp", "slug": "acme-corp", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": null, "currency": "USD", "allowSupportAccess": false } ``` ## Next steps Send your first message Understand the data model All error types and recovery API limits and backoff # Team setup Source: https://docs.inboxapp.com/configuration/team-setup Managing team members and roles in Inboxapp ## Overview Teams in Inboxapp organize your account links, prospects, and conversations. This guide covers retrieving team information and managing team members via the API. ## Get team information Retrieve your team details: ```typescript team.ts theme={null} const { data: team } = await client.get("/team"); console.log("Team:", team.name); console.log("Currency:", team.currency); console.log("Created:", team.createdAt); ``` ```bash cURL theme={null} curl https://inboxapp.com/api/v1/team \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} { "id": "hzcai5t59nn9vsck3rbuepyg", "name": "Acme Corp", "slug": "acme-corp", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": null, "currency": "USD", "allowSupportAccess": false } ``` | Field | Description | | -------------------- | ----------------------------------------------------------- | | `id` | Inbox team ID | | `name` | Team display name | | `slug` | URL-friendly team identifier | | `currency` | Currency used for prospect valuations (e.g., `"USD"`) | | `allowSupportAccess` | Whether the Inboxapp support team can access your workspace | ## List team members Get all members of your team: ```typescript members.ts theme={null} const { data: members } = await client.get("/members"); console.log(`Team has ${members.length} members`); members.forEach((member) => { console.log(`${member.name} (${member.email}) - ${member.role}`); }); ``` ```bash cURL theme={null} curl https://inboxapp.com/api/v1/members \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} [ { "id": "r3km7xj9wq5p2bvnhfdteoly", "userId": "usr4hn8xj2wq5p1bvnhfdteo", "name": "Jane Smith", "email": "jane@acmecorp.com", "image": "https://lh3.googleusercontent.com/...", "role": "owner", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": null }, { "id": "s4ln8yk0xr6q3cw1behsifup", "userId": "usr5io9yk3xr6q2cw1behsifu", "name": "John Doe", "email": "john@acmecorp.com", "image": "https://lh3.googleusercontent.com/...", "role": "admin", "createdAt": "2024-02-01T09:00:00.000Z", "updatedAt": null }, { "id": "t5mo9zl1ys7r4dx2cfitjgvqw", "userId": "usr6jp0zl4ys7r3dx2cfitjgv", "name": "Alice Johnson", "email": "alice@acmecorp.com", "image": null, "role": "user", "createdAt": "2024-02-15T14:00:00.000Z", "updatedAt": null } ] ``` `GET /members` returns a flat array, not a wrapped object. The same applies to `GET /account-links`, `GET /tags`, and `GET /statuses`. ## Get specific member Retrieve details for a specific member: ```typescript theme={null} const { data: member } = await client.get(`/members/${memberId}`); console.log("Member:", member.name); ``` ## Member roles | Role | Permissions | | ------- | ---------------------------------------------- | | `owner` | Full access, billing management, team deletion | | `admin` | Full access except billing and team deletion | | `user` | Access to assigned threads and features | ## Thread assignment Assign threads to team members for workload distribution: ```typescript theme={null} // Assign a thread to a member await client.patch(`/threads/${threadId}`, { assigneeId: "r3km7xj9wq5p2bvnhfdteoly", }); // Unassign a thread await client.patch(`/threads/${threadId}`, { assigneeId: null, }); ``` ## Filter by assignee Get threads assigned to specific members: ```typescript theme={null} // Get threads assigned to a specific member const { data } = await client.get("/threads", { params: { "filters[assignees][selectedIds][0]": "r3km7xj9wq5p2bvnhfdteoly", }, }); // Get unassigned threads const { data: unassigned } = await client.get("/threads", { params: { "filters[assignees][noAssignee]": true, }, }); ``` ## Common patterns ### Round-robin assignment Distribute new threads evenly across team members: ```typescript theme={null} async function getTeamMembers(): Promise { const { data: members } = await client.get("/members"); return members.filter((m: any) => m.role !== "owner").map((m: any) => m.id); } class RoundRobinAssigner { private members: string[] = []; private currentIndex = 0; async initialize() { this.members = await getTeamMembers(); } next(): string { const memberId = this.members[this.currentIndex]; this.currentIndex = (this.currentIndex + 1) % this.members.length; return memberId; } } // Usage const assigner = new RoundRobinAssigner(); await assigner.initialize(); async function assignNewThread(threadId: string) { const memberId = assigner.next(); await client.patch(`/threads/${threadId}`, { assigneeId: memberId, }); console.log(`Assigned thread to ${memberId}`); } ``` ### Workload-based assignment Assign to the team member with the fewest open threads. Since the API doesn't return a total count, you need to paginate to get real numbers: ```typescript theme={null} async function countThreadsForMember(memberId: string): Promise { let count = 0; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get("/threads", { params: { "filters[assignees][selectedIds][0]": memberId, inbox: "default", limit: 100, ...(cursorId && { cursorId, cursorTimestamp }), }, }); count += data.threads.length; cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return count; } async function assignToLeastBusy(threadId: string) { const { data: members } = await client.get("/members"); let minMember = ""; let minCount = Infinity; for (const member of members) { if (member.role === "owner") continue; const count = await countThreadsForMember(member.id); if (count < minCount) { minCount = count; minMember = member.id; } } if (minMember) { await client.patch(`/threads/${threadId}`, { assigneeId: minMember, }); console.log(`Assigned to ${minMember} (${minCount} open threads)`); } } ``` This makes multiple API calls per member. For teams with many members or threads, consider caching workload counts and refreshing periodically rather than computing on every assignment. ### Role-based routing Route threads based on message content to appropriate team members: ```typescript theme={null} interface RoutingConfig { memberId: string; keywords: string[]; } const routingRules: RoutingConfig[] = [ { memberId: "s4ln8yk0xr6q3cw1behsifup", keywords: ["pricing", "demo", "quote", "buy", "purchase"], }, { memberId: "t5mo9zl1ys7r4dx2cfitjgvqw", keywords: ["help", "issue", "problem", "bug", "error", "not working"], }, ]; async function routeByContent(threadId: string, messageText: string) { const text = messageText.toLowerCase(); for (const rule of routingRules) { if (rule.keywords.some((k) => text.includes(k))) { await client.patch(`/threads/${threadId}`, { assigneeId: rule.memberId, }); console.log(`Routed to ${rule.memberId} based on keywords`); return rule.memberId; } } return null; } ``` ## Best practices Use workload-based or round-robin assignment to prevent bottlenecks. Each thread should have a clear owner. Avoid leaving threads unassigned for too long. ## Related endpoints GET /team GET /members GET /members/ PATCH /threads/ ## Next steps Connect X accounts Thread operations and filtering Manage prospect data Organize prospects # Overview Source: https://docs.inboxapp.com/developer-api The API for sending and managing DM conversations across X, Instagram, LinkedIn, and more ## What is Inboxapp? Inboxapp is the API for sending and managing DM conversations across messaging platforms — starting with X (Twitter), with Instagram and LinkedIn coming Q2 2026. Send and receive DMs through a unified API, sync conversations with your systems, and build custom workflows. Your team also gets a fast inbox interface to manage conversations, assign threads, and collaborate in real time. **Platform support**: The API currently supports X (Twitter) DMs. Instagram and LinkedIn are coming in Q2 2026. See [Supported platforms](/resources/supported-platforms) for the full roadmap. **For teams:** * Manage all X DM conversations in one place * Assign, tag, and track conversations * Organize with tags and workflow stages * Real-time collaboration **For developers:** * Programmatic access to all conversations * Reliable message delivery * Sync data with external systems * Build custom integrations ## What you can build Send DMs programmatically. Build custom send workflows, schedule messages, and handle delivery. Sync conversations to HubSpot, Salesforce, or your internal tools. Keep your systems in sync. Build intelligent auto-replies. Answer questions, handle conversations, and route complex queries to your team. Track response rates, measure team performance, and build analytics on top of your DM data. ## Get started with the API **Prerequisites:** An Inboxapp account with API access enabled. **Set your API token:** ```bash theme={null} export INBOX_API_TOKEN="your_token_here" ``` **Make your first request:** ```bash theme={null} curl https://inboxapp.com/api/v1/team \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` Send your first DM in 5 minutes Get your API token ## Next steps Understand threads, prospects, and the data model Explore all endpoints Work with conversations Get notified when events happen in real time Common issues and fixes ## Why teams choose Inboxapp Assign conversations, track who's handling what, and collaborate without stepping on each other's toes. Tags and workflow stages keep your conversations organized. Automate message delivery, CRM syncing, conversation routing — and keep the human touch for what matters. Role-based access, team-wide rate limits, and multi-account support. Connect multiple X accounts and manage them from one workspace. # Automate with the API Source: https://docs.inboxapp.com/get-started/automate Get an API token, make your first call, and connect Inboxapp to your tools. For developers with an Inboxapp account; you need an API token. 1. [Create an API token](/configuration/authentication). Keep it secret and use it in the Authorization header. 2. [Make your first API call](/quickstart). The team request confirms that your token works. 3. Connect AI tools with the [MCP server](/mcp-server). It uses the same API token as the REST API. 4. Continue with the [Developer API](/developer-api). Find the data model, webhooks, reference, and endpoint listings there. Next: [Return to the Get started welcome page](/). # Send your first outreach Source: https://docs.inboxapp.com/get-started/first-outreach Create a lead list, import leads, and launch your first Inboxapp campaign. For teams that have finished setup; campaigns are available on Pro. 1. [Create your first lead list](/help/leads-management/organize/create-your-first-lead-list). The list is ready to receive prospects. 2. [Import leads](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) from a supported source. New usable profiles use Pro credits. 3. [Create your first campaign](/help/campaigns/build/create-your-first-campaign). The campaign begins in the Pending stage until it starts sending. 4. [Read the campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp). You can track pending, in-sequence, replied, and other outcomes. Campaigns are available on Pro. See [Plans and credits](/help/billing/billing-plans-and-credits). Next: [Automate with the API](/get-started/automate). # Set up Inboxapp Source: https://docs.inboxapp.com/get-started/set-up Connect X, confirm DM sync, and personalize your Inboxapp workspace. For teams getting started with Inboxapp; you need Chrome and an X account. 1. [Install the Chrome extension and link your X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp). Linking starts the initial DM sync. 2. If prompted, [enter your XChat PIN](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform). This lets Inboxapp sync XChat DMs. 3. [Confirm your DMs are syncing](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect). You can read and organize messages while the sync continues. 4. [Personalize statuses and tags](/help/getting-started-with-inboxapp/personalize-inboxapp-to-your-needs). Your workspace is ready for lead lists and outreach. Next: [Send your first outreach](/get-started/first-outreach). # Core concepts Source: https://docs.inboxapp.com/guides/core-concepts Understanding the Inboxapp API data model ## Overview The Inboxapp API manages DM conversations across messaging platforms. Understanding these core entities will help you build effective integrations. **Multi-Platform Future**: The API currently supports X (Twitter) DMs. Instagram and LinkedIn are coming in Q2 2026. The data model is platform-agnostic — see [Supported platforms](/resources/supported-platforms). ## Key entities ### Threads A **thread** represents a DM conversation between one of your account links and a prospect. ```json theme={null} { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": null, "lastMessageTimestamp": "2025-01-15T15:30:00.000Z", "computedSortTimestamp": "2025-01-15T15:30:00.000Z", "createdAt": "2025-01-10T10:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Sounds great, let's schedule a call!", "authorId": "hzcai5t59nn9vsck3rbuepyg", "createdAt": "2025-01-15T15:30:00.000Z", "userId": null, "campaignId": null }, "prospect": { "..." } } ``` Key fields: | Field | Description | | --------------- | ---------------------------------------------------------------------------- | | `id` | Inbox thread ID — use this for all API operations | | `platform` | The platform this thread belongs to (currently `"twitter"`) | | `platformId` | The X conversation ID (formatted as `smallerUserId:largerUserId`) | | `done` | Whether the thread is archived | | `assigneeId` | Team member assigned to this thread (null if unassigned) | | `status` | `"active"` (visible in inbox) or `"idle"` (hidden, e.g. Quick Peek previews) | | `variant` | `"unencrypted"` (legacy DMs) or `"xChat"` (encrypted DMs) | | `accountLinkId` | Which connected X account this thread belongs to | | `isRequest` | Whether this is a message request from an unknown prospect | | `lastMessage` | Preview of the most recent message | | `prospect` | The full prospect object (with context) embedded in the thread | ### Messages **Messages** are individual DMs sent or received in a thread. ```json theme={null} { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": "r3km7xj9wq5p2bvnhfdteoly", "campaignId": null, "content": "Thanks for reaching out! I'd love to learn more about your project.", "origin": "api", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": null, "isEdited": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null } ``` Key fields: | Field | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `content` | The message text | | `authorId` | Either the `accountLinkId` or the prospect's `externalId` — identifies the sender within Inboxapp (not a platform ID) | | `userId` | Inbox user ID if sent from Inboxapp by a team member. `null` if the message is from the prospect, was sent from the X client, or was otherwise sent outside of Inboxapp. A `null` value does not necessarily mean the message is from the prospect — it just means it wasn't sent from Inboxapp. | | `origin` | Where the message originated: `"external"` (pulled from X), `"internal"` (sent from the Inboxapp UI or a campaign), `"api"` (sent via the API), or `null` | | `campaignId` | Non-null if the message was sent by a campaign (these have `origin: "internal"`) | | `isEdited` | Whether the message has been edited | There is no `direction` field on messages. Check `authorId` against your account link IDs to determine the sender. See [Working with messages — Determining message direction](/guides/messages#determining-message-direction) for details. ### Prospects A **prospect** represents an external user on a messaging platform. ```json theme={null} { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "hzcai5t59nn9vsck3rbuepyg", "documentId": 858224163, "displayName": "Jordan Rivera", "username": "jordanrivera", "handle": "@jordanrivera", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/.../photo.jpg", "bio": "Head of Growth at TechCo. Building the future of SaaS.", "location": "San Francisco, CA", "profileUrl": "https://x.com/jordanrivera", "websiteUrl": "https://techco.com", "websiteDomain": "techco.com", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12400, "followingCount": 892, "postCount": 3420, "engagementCount": 8100, "listedCount": 18, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2024-07-09T14:08:49.000Z", "lastUpdatedAt": "2025-01-15T10:30:00.000Z", "lastEnrichedAt": "2025-01-15T10:30:00.000Z", "lastActiveAt": "2025-01-14T12:00:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.85, "platformData": { "isVerifiedBlue": false, "isVerifiedGold": false, "isVerifiedGray": false, "professionalCategory": null, "urlEntities": [], "bannerUrl": "https://pbs.twimg.com/profile_banners/...", "tweetsCount": 3420, "favoritesCount": 8100, "rawData": null } } ``` Key differences from what you might expect: | Field | Not this | It's this | | ------------ | ----------------- | ---------------------------------------------------------------------------------- | | Display name | `name` | `displayName` | | Bio | `description` | `bio` | | Avatar | `profileImageUrl` | `image` (or `imageNormalized` for highest quality) | | Followers | `followersCount` | `followerCount` (singular) | | Verified | `true` / `false` | `"verified"` (blue check), `"business"` (gold), `"government"` (gray), or `"none"` | | Profile type | — | `"personal"`, `"business"`, or `"government"` | | Source | — | `"cached"`, `"indexed"`, `"live"`, or `"merged"` | Prospects are shared across all account links in your team. When retrieved as part of a thread or via the prospects endpoints, they include a `context` object with your team's custom data. ### Prospect context When you retrieve a prospect (via threads or the prospects API), the response includes a `context` object: ```json theme={null} { "context": { "tags": ["t8rvk2m5j0xn4wq7ybftcael"], "statusId": "s3km7xj9wq5p2bvnhfdteoly", "valuation": 50000, "notes": "Decision maker at TechCo. Follow up next week." } } ``` | Field | Type | Description | | ----------- | ---------------- | ----------------------------------------- | | `tags` | `string[]` | Array of tag IDs applied to this prospect | | `statusId` | `string \| null` | Pipeline status ID | | `valuation` | `number \| null` | Deal value | | `notes` | `string \| null` | Free-text notes | Update context with `PATCH /prospects/{prospectId}/context`: ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { notes: "Interested in Enterprise plan", valuation: 50000, statusId: "s3km7xj9wq5p2bvnhfdteoly", addTags: ["t8rvk2m5j0xn4wq7ybftcael"], removeTags: ["old_tag_id_here"], }); ``` Tags are modified with `addTags` and `removeTags` — there is no `tagIds` field. See [Tags & statuses](/guides/tags-and-statuses) for details. ### Account links An **account link** is an X account connected to your team. ```json theme={null} { "id": "df6jbw4h36qm5d9iu2sgn7kx", "platform": "twitter", "platformId": "1566123362161725440", "name": "Acme Corp", "username": "acmecorp", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "createdAt": "2024-06-01T12:00:00.000Z", "status": "active", "syncedAt": "2025-01-15T10:30:00.000Z" } ``` Use account links to: * Send messages from specific accounts * Filter threads by account * Manage multi-account workflows ### Tags **Tags** are flexible labels you apply to prospects for organization. ```json theme={null} { "id": "t8rvk2m5j0xn4wq7ybftcael", "name": "Hot Lead", "color": "#ef4444" } ``` A prospect can have multiple tags. Colors accept either a **predefined name** (e.g., `"red"`, `"blue"`, `"green"`) or a **hex code** (e.g., `"#ef4444"`). Use `GET /colors` to see all predefined options. ### Statuses **Statuses** represent pipeline stages for a prospect. ```json theme={null} { "id": "s3km7xj9wq5p2bvnhfdteoly", "name": "Qualified", "color": "#f59e0b", "createdAt": "2024-06-01T12:00:00.000Z" } ``` A prospect can have only one status at a time. Use statuses for linear progression (e.g., New → Qualified → Won). Use tags for non-linear attributes. ### Members **Members** are users on your team. ```json theme={null} { "id": "r3km7xj9wq5p2bvnhfdteoly", "userId": "usr4hn8xj2wq5p1bvnhfdteo", "name": "Alex Chen", "email": "alex@acmecorp.com", "image": "https://lh3.googleusercontent.com/...", "role": "admin", "createdAt": "2024-06-01T12:00:00.000Z", "updatedAt": null } ``` Roles are `"owner"`, `"admin"`, or `"user"`. Member IDs are used for assigning threads and filtering by assignee. ## The dual ID system Inboxapp uses two types of IDs for platform-related objects: e.g. `l44e15irdq4db30i77cgphhx` The internal Inbox ID (CUID2 format). Use this for all API operations. e.g. `1876543210987654321` The original ID from X. Use this for lookups and constructing X URLs. ```typescript theme={null} // Use Inbox IDs for API operations await client.get(`/threads/${thread.id}/messages`); // Use Platform IDs for lookups await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321", accountLinkId: account.id, }, }); ``` ### Helpful X URLs You can use platform IDs to construct direct links to X: | Link type | URL pattern | | -------------------------- | ----------------------------------------------- | | User profile (by ID) | `https://x.com/i/user/{platformId}` | | User profile (by username) | `https://x.com/{username}` | | Tweet/Post | `https://x.com/anyusername/status/{tweetId}` | | DM conversation | `https://x.com/i/chat/{conversationPlatformId}` | The user-by-ID link (`/i/user/{platformId}`) is the most reliable since usernames can change. For tweet URLs, X ignores the username — only the tweet ID matters. ## Thread status vs inbox views The thread `status` field (`"active"` or `"idle"`) controls whether the thread is visible in the inbox. Active threads appear in inbox views; idle threads are hidden (e.g., threads created via Quick Peek that haven't had a real conversation yet). This is separate from **inbox views**, which are filtered using the `inbox` query parameter: | `inbox` value | Description | | ------------- | ------------------------------------------------------------- | | `"default"` | Active conversations needing attention | | `"no-reply"` | The prospect sent the last message and is awaiting your reply | | `"requests"` | New message requests from unknown prospects | | `"archived"` | Threads marked as done (`done: true`) | Filter by inbox view: ```typescript theme={null} // Active conversations const { data } = await client.get("/threads", { params: { inbox: "default" }, }); // Threads where the prospect is awaiting your reply const { data: noReply } = await client.get("/threads", { params: { inbox: "no-reply" }, }); // Archived threads const { data: archived } = await client.get("/threads", { params: { inbox: "archived" }, }); ``` ## Response shapes Different endpoints return data in different shapes. Here's what to expect: | Pattern | Endpoints | Shape | | ------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- | | Single object | `GET /threads/{id}`, `POST /threads`, `GET /threads/lookup` | Returns the object directly | | Array | `GET /tags`, `GET /statuses`, `GET /account-links`, `GET /members` | Returns an array directly | | Paginated | `GET /threads`, `GET /threads/{id}/messages` | Returns `{ threads, nextCursor }` or `{ messages, nextCursor }` | ```typescript theme={null} // Direct object — no wrapper const { data: thread } = await client.get(`/threads/${threadId}`); console.log(thread.id); // Direct array — no wrapper const { data: tags } = await client.get("/tags"); console.log(tags.length); // Paginated — has wrapper const { data } = await client.get("/threads"); console.log(data.threads.length, data.nextCursor); ``` ## Pagination List endpoints use cursor-based pagination: ```typescript theme={null} const allThreads = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get("/threads", { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }), }, }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); ``` Use the flat `cursorId` and `cursorTimestamp` parameters instead of bracket notation (`cursor[id]`). The bracket notation form is deprecated. ## Entity relationships ``` Team ├── Members (team users) ├── Account Links (connected X accounts) ├── Tags (labels for prospects) ├── Statuses (pipeline stages) └── Prospects (external X users) ├── Context (tags, status, notes, valuation) └── Threads (one per account link + prospect pair) └── Messages ``` * A prospect can have **multiple tags** but only **one status** * A thread belongs to exactly **one account link** and **one prospect** * A prospect can have threads across **multiple account links** ## Next steps Thread operations and filtering Send and receive messages Manage prospect data and context Organize with tags and pipeline stages # Managing threads Source: https://docs.inboxapp.com/guides/managing-threads Working with conversations in the Inboxapp API ## Listing threads Retrieve threads with filtering and pagination: ```typescript list.ts theme={null} const { data } = await client.get("/threads", { params: { limit: 20, inbox: "default", }, }); console.log(`Found ${data.threads.length} threads`); data.threads.forEach((thread) => { console.log(`${thread.id} — @${thread.prospect.username}`); }); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/threads?inbox=default&limit=20" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} { "threads": [ { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "done": false, "assigneeId": null, "lastMessageTimestamp": "2025-01-15T15:30:00.000Z", "computedSortTimestamp": "2025-01-15T15:30:00.000Z", "createdAt": "2025-01-10T10:00:00.000Z", "status": "active", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Sounds great, let's schedule a call!", "authorId": "hzcai5t59nn9vsck3rbuepyg", "createdAt": "2025-01-15T15:30:00.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "displayName": "Jordan Rivera", "username": "jordanrivera", "handle": "@jordanrivera", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "bio": "Head of Growth at TechCo", "followerCount": 12400, "verified": "none", "context": { "tags": [], "statusId": null, "valuation": null, "notes": null }, "..." } } ], "nextCursor": { "id": "l44e15irdq4db30i77cgphhx", "timestamp": "2025-01-15T15:30:00.000Z" } } ``` The default page size is 20 (max 100). Use `nextCursor` to paginate. ## Looking up threads by username Find all threads for a prospect by their exact platform username: ```typescript lookup-username.ts theme={null} const { data: threads } = await client.get("/threads/lookup-by-username", { params: { username: "jordanrivera", }, }); threads.forEach((thread) => { console.log(`${thread.id} — ${thread.accountLinkId}`); }); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/threads/lookup-by-username?username=jordanrivera" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` Returns a flat array of `Thread` objects — one per account link that has a conversation with that prospect. No pagination; all matching threads are returned. ### Filter by account link Scope results to specific connected accounts: ```typescript theme={null} const { data: threads } = await client.get("/threads/lookup-by-username", { params: { username: "jordanrivera", "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx", }, }); ``` The `username` must be an exact match (case-insensitive, without the `@` prefix). For lookup by platform ID, use [GET /threads/lookup](/api-reference/threads/lookup-thread) instead. ## Filtering threads ### By inbox view The `inbox` parameter filters by thread state: ```typescript theme={null} // Active conversations const active = await client.get("/threads", { params: { inbox: "default" } }); // Prospect sent the last message — you need to reply const noReply = await client.get("/threads", { params: { inbox: "no-reply" } }); // New message requests const requests = await client.get("/threads", { params: { inbox: "requests" }, }); // Archived (done) const archived = await client.get("/threads", { params: { inbox: "archived" }, }); ``` ### By account link Filter threads to specific connected accounts. Pass an array of IDs via `accountLinkIds`: ```typescript theme={null} const { data } = await client.get("/threads", { params: { "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx", }, }); ``` ### By tags ```typescript theme={null} // Prospects with ANY of these tags const { data } = await client.get("/threads", { params: { "filters[tags][selectedIds][0]": "t8rvk2m5j0xn4wq7ybftcael", "filters[tags][selectedIds][1]": "u9swl3n6k1yo5xr8zcgudbfm", }, }); // Prospects with NO tags const { data: untagged } = await client.get("/threads", { params: { "filters[tags][noTags]": true, }, }); ``` ### By status ```typescript theme={null} // Threads with prospects in specific statuses const { data } = await client.get("/threads", { params: { "filters[statuses][selectedIds][0]": "s3km7xj9wq5p2bvnhfdteoly", }, }); // Prospects with no status const { data: noStatus } = await client.get("/threads", { params: { "filters[statuses][noStatus]": true, }, }); ``` ### By assignee ```typescript theme={null} // Assigned to specific member const { data } = await client.get("/threads", { params: { "filters[assignees][selectedIds][0]": "r3km7xj9wq5p2bvnhfdteoly", }, }); // Unassigned threads const { data: unassigned } = await client.get("/threads", { params: { "filters[assignees][noAssignee]": true, }, }); ``` ### By campaign ```typescript theme={null} // Threads from specific campaigns const { data } = await client.get("/threads", { params: { "filters[campaigns][selectedIds][0]": "c5nwp8r2j3ym6at0xbkqhfvg", }, }); // Threads not from any campaign const { data: noCampaign } = await client.get("/threads", { params: { "filters[campaigns][noCampaign]": true, }, }); ``` ### Sort order ```typescript theme={null} const { data } = await client.get("/threads", { params: { "filters[sortOrder]": "desc", // or 'asc' }, }); ``` ### Combined filters Filters combine with AND logic: ```typescript theme={null} // Active threads, qualified status, assigned to a specific member const { data } = await client.get("/threads", { params: { inbox: "default", "accountLinkIds[0]": "df6jbw4h36qm5d9iu2sgn7kx", "filters[statuses][selectedIds][0]": "s3km7xj9wq5p2bvnhfdteoly", "filters[assignees][selectedIds][0]": "r3km7xj9wq5p2bvnhfdteoly", limit: 20, }, }); ``` ### Filter helper A utility to build filter params: ```typescript theme={null} function buildFilters(opts: { tagIds?: string[]; statusIds?: string[]; assigneeIds?: string[]; campaignIds?: string[]; noTags?: boolean; noStatus?: boolean; noAssignee?: boolean; noCampaign?: boolean; sortOrder?: "asc" | "desc"; }) { const params: Record = {}; opts.tagIds?.forEach((id, i) => { params[`filters[tags][selectedIds][${i}]`] = id; }); opts.statusIds?.forEach((id, i) => { params[`filters[statuses][selectedIds][${i}]`] = id; }); opts.assigneeIds?.forEach((id, i) => { params[`filters[assignees][selectedIds][${i}]`] = id; }); opts.campaignIds?.forEach((id, i) => { params[`filters[campaigns][selectedIds][${i}]`] = id; }); if (opts.noTags) params["filters[tags][noTags]"] = true; if (opts.noStatus) params["filters[statuses][noStatus]"] = true; if (opts.noAssignee) params["filters[assignees][noAssignee]"] = true; if (opts.noCampaign) params["filters[campaigns][noCampaign]"] = true; if (opts.sortOrder) params["filters[sortOrder]"] = opts.sortOrder; return params; } const { data } = await client.get("/threads", { params: { inbox: "default", ...buildFilters({ tagIds: ["t8rvk2m5j0xn4wq7ybftcael"], noAssignee: true }), limit: 50, }, }); ``` ## Pagination Use `cursorId` and `cursorTimestamp` to paginate through all threads: ```typescript theme={null} async function getAllThreads() { const allThreads: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get("/threads", { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }), }, }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return allThreads; } ``` See [Pagination](/reference/pagination) for streaming patterns and error handling. ## Looking up a thread Find an existing thread between an account and a prospect: ```typescript lookup.ts theme={null} const { data: thread } = await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", }, }); if (thread) { console.log("Found thread:", thread.id); } else { console.log("No thread exists yet"); } ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/threads/lookup?externalPlatformId=1876543210987654321&accountLinkId=df6jbw4h36qm5d9iu2sgn7kx" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` The lookup endpoint accepts either `externalPlatformId` (X user ID) or `externalId` (Inbox external ID). At least one must be provided along with `accountLinkId`. Returns the `Thread` object directly, or `null` if no thread exists. ## Creating a thread Create a new conversation thread: ```typescript theme={null} const { data: thread } = await client.post("/threads", { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", }); console.log("Created thread:", thread.id); ``` Creating a thread doesn't send a message. Use the [messages endpoint](/api-reference/messages/send-message) to send a DM. If a thread already exists between the account link and prospect, the API will return an error. Use [lookup](/api-reference/threads/lookup-thread) first to check, or use the [lookup-or-create pattern](#lookup-or-create-pattern) below. ## Getting thread details ```typescript theme={null} const { data: thread } = await client.get(`/threads/${threadId}`); console.log("Thread:", thread.id); console.log("Prospect:", thread.prospect.displayName); console.log("Last message:", thread.lastMessage?.content); ``` Returns the `Thread` object directly — not wrapped in `{ thread: ... }`. ## Updating threads Modify thread assignment and done status: ```typescript theme={null} // Assign thread to a team member await client.patch(`/threads/${threadId}`, { assigneeId: "r3km7xj9wq5p2bvnhfdteoly", }); // Mark thread as done (archives it) await client.patch(`/threads/${threadId}`, { done: true, }); // Unassign and reopen await client.patch(`/threads/${threadId}`, { assigneeId: null, done: false, }); ``` ## Deleting threads Soft-delete a thread: ```typescript theme={null} await client.delete(`/threads/${threadId}`); ``` Deletion is permanent and cannot be undone. The thread is also deleted on the X platform. Consider using `done: true` to archive instead. ## Common workflows ### Lookup-or-create pattern ```typescript theme={null} async function getOrCreateThread( prospectPlatformId: string, accountLinkId: string, ) { const { data: existing } = await client.get("/threads/lookup", { params: { externalPlatformId: prospectPlatformId, accountLinkId }, }); if (existing) return existing; const { data: created } = await client.post("/threads", { externalPlatformId: prospectPlatformId, accountLinkId, }); return created; } ``` ### Round-robin assignment ```typescript theme={null} const memberIds = ["r3km7xj9wq5p2bvnhfdteoly", "s4ln8yk0xr6q3cw1behsifup", "t5mo9zl1ys7r4dx2cfitjgvqw"]; let idx = 0; async function assignRoundRobin(threadId: string) { await client.patch(`/threads/${threadId}`, { assigneeId: memberIds[idx], }); idx = (idx + 1) % memberIds.length; } ``` ### Get threads needing follow-up ```typescript theme={null} const { data } = await client.get("/threads", { params: { inbox: "no-reply", limit: 50, }, }); console.log(`${data.threads.length} threads awaiting your reply`); ``` ## Related endpoints GET /threads GET /threads/lookup-by-username POST /threads GET /threads/lookup PATCH /threads/ DELETE /threads/ Send and receive messages in threads # Working with messages Source: https://docs.inboxapp.com/guides/messages Sending and retrieving messages in the Inboxapp API ## Sending messages Two ways to send messages: within an existing thread, or using quick send. **Sending first messages requires the Outbound Messages addon.** Replying to contacts who have already messaged you works on all plans. Sending the first message to a new contact requires the Outbound Messages addon (\$199/mo) on a paid plan. Enable the addon in your [Inboxapp dashboard](https://inboxapp.com) under **Settings → Billing**, or contact [support@inboxapp.com](mailto:support@inboxapp.com) for help. ### Standard send (existing thread) Send a message in a thread you've already created or looked up: ```typescript send.ts theme={null} const { data: message } = await client.post(`/threads/${threadId}/messages`, { content: 'Thanks for your interest! How can I help you today?' }); console.log('Message sent:', message.id); ``` ```bash cURL theme={null} curl -X POST "https://inboxapp.com/api/v1/threads/THREAD_ID/messages" \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"content": "Thanks for your interest! How can I help you today?"}' ``` **Response:** ```json theme={null} { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": "r3km7xj9wq5p2bvnhfdteoly", "campaignId": null, "content": "Thanks for your interest! How can I help you today?", "origin": "api", "createdAt": "2025-01-15T16:45:00.000Z", "updatedAt": null, "isEdited": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null } ``` The send message endpoint returns the `Message` object directly — not wrapped in `{ message: ... }`. ### Quick send Quick send combines thread lookup/creation and message sending in one request: ```typescript quick-send.ts theme={null} const { data } = await client.post('/threads/messages', { externalPlatformId: '1876543210987654321', accountLinkId: 'df6jbw4h36qm5d9iu2sgn7kx', content: 'Hi! I saw your post and wanted to reach out.' }); console.log('Message:', data.message.id); console.log('Thread:', data.thread.id); ``` ```bash cURL theme={null} curl -X POST "https://inboxapp.com/api/v1/threads/messages" \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalPlatformId": "1876543210987654321", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "content": "Hi! I saw your post and wanted to reach out." }' ``` **Response:** ```json theme={null} { "message": { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "threadId": "l44e15irdq4db30i77cgphhx", "content": "Hi! I saw your post and wanted to reach out.", "origin": "api", "createdAt": "2025-01-15T16:45:00.000Z", "..." }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321" } } ``` Quick send accepts either `externalPlatformId` (the X user ID) or `externalId` (the Inbox external ID) to identify the prospect. **When to use quick send:** * One-off messages where you don't need thread details * Simplifying your code flow **When to use standard send:** * Sending multiple messages in sequence * You're already working with a thread object ## Retrieving messages Get message history for a thread: ```typescript list.ts theme={null} const { data } = await client.get(`/threads/${threadId}/messages`, { params: { limit: 50 } }); console.log(`Found ${data.messages.length} messages`); const accountLinkIds = new Set(yourAccountLinks.map(a => a.id)); data.messages.forEach(message => { const sender = accountLinkIds.has(message.authorId) ? 'You' : 'Prospect'; console.log(`${sender}: ${message.content}`); }); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/threads/THREAD_ID/messages?limit=50" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} { "teamId": "hzcai5t59nn9vsck3rbuepyg", "threadId": "l44e15irdq4db30i77cgphhx", "messages": [ { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Great! Let me tell you more about it", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": "r3km7xj9wq5p2bvnhfdteoly", "origin": "internal", "createdAt": "2025-01-15T10:05:00.000Z", "..." }, { "id": "q7wmx3n6k1yo5xr8zcgudbfm", "content": "Hi, I'm interested in your product", "authorId": "hzcai5t59nn9vsck3rbuepyg", "userId": null, "origin": "external", "createdAt": "2025-01-15T10:00:00.000Z", "..." } ], "nextCursor": { "id": "q7wmx3n6k1yo5xr8zcgudbfm", "timestamp": "2025-01-15T10:00:00.000Z" } } ``` ## Determining message direction There is no `direction` field on messages. Use `authorId` to determine who sent a message: ```typescript theme={null} // authorId will be either the accountLinkId (your account) or the prospect's externalId const accountLinkIds = new Set(yourAccountLinks.map(a => a.id)); if (accountLinkIds.has(message.authorId)) { console.log('Sent by our account'); } else { console.log('Received from prospect'); } // Sent via the API if (message.origin === 'api') { console.log('Sent via API'); } // Sent by a campaign if (message.campaignId) { console.log('Sent by campaign:', message.campaignId); } ``` Don't rely on `userId` to determine message direction. `userId` is `null` for messages sent from the X mobile app or web client — even if they were sent by your account. Always check `authorId` against your account link IDs instead. | `origin` value | Meaning | | -------------- | ----------------------------------------------------------------------------------- | | `"external"` | Pulled from X — the message was discovered on the platform and synced into Inboxapp | | `"internal"` | Sent from the Inboxapp UI or a campaign | | `"api"` | Sent via the Inboxapp API | `origin` describes *where the message entered Inboxapp*, not who sent it. An `"external"` message could be from the prospect or from your account (sent via the X client). Campaign messages have `origin: "internal"` with a non-null `campaignId`. Check `campaignId` to distinguish campaign messages from messages sent manually through the Inboxapp UI. ## Pagination For long conversations, paginate through messages using `cursorId` and `cursorTimestamp`: ```typescript theme={null} async function getAllMessages(threadId: string) { const allMessages: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get(`/threads/${threadId}/messages`, { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); allMessages.push(...data.messages); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return allMessages; } ``` See [Pagination](/reference/pagination) for streaming patterns and error handling. ## Replying to a specific message You can reply to a specific message by providing `replyToMessageId`: ```typescript theme={null} const { data: reply } = await client.post(`/threads/${threadId}/messages`, { content: 'Great question — here are the details...', replyToMessageId: 'q7wmx3n6k1yo5xr8zcgudbfm' }); ``` ## Editing messages Edit a message in an X Chat (encrypted DM) thread: ```typescript theme={null} const { data } = await client.patch( `/threads/${threadId}/messages/${messageId}`, { content: 'Updated message content' } ); console.log('Edited at:', data.editedAt); ``` Editing is only supported for **X Chat** (encrypted DM) threads. Check the thread's `variant` field — if it's `"unencrypted"`, the API will throw an error. The message must have existing text content to be eligible for editing. If a message was sent with only an attachment and no text content, it cannot be edited. ## Deleting messages Delete a message in an X Chat thread: ```typescript theme={null} const { data } = await client.delete( `/threads/${threadId}/messages/${messageId}`, { data: { deleteForAll: true } } ); console.log('Deleted at:', data.deletedAt); ``` Deleting is only supported for **X Chat** (encrypted DM) threads. Check the thread's `variant` field — if it's `"unencrypted"`, the API will throw an error. ## Reactions Add or remove emoji reactions on messages: ```typescript theme={null} // Add a reaction await client.post( `/threads/${threadId}/messages/${messageId}/reactions`, { emoji: '👍' } ); // Remove a reaction await client.delete( `/threads/${threadId}/messages/${messageId}/reactions`, { data: { emoji: '👍' } } ); ``` ## Message history Get the edit history and deletion status of a message: ```typescript theme={null} const { data } = await client.get( `/threads/${threadId}/messages/${messageId}/history` ); console.log('Versions:', data.versions.length); if (data.deletion) { console.log('Deleted at:', data.deletion.deletedAt); } ``` ## Common workflows ### Send a follow-up ```typescript theme={null} // Cache your account link IDs at startup const { data: accountLinks } = await client.get('/account-links'); const accountLinkIds = new Set(accountLinks.map(a => a.id)); async function sendFollowUp(threadId: string) { const { data } = await client.get(`/threads/${threadId}/messages`, { params: { limit: 1 } }); const lastMessage = data.messages[0]; if (lastMessage && accountLinkIds.has(lastMessage.authorId)) { // Last message was from one of our accounts — follow up await client.post(`/threads/${threadId}/messages`, { content: 'Just following up on my previous message. Let me know if you have any questions!' }); } } ``` Don't use `userId` to check if the last message is from your team. Your team member may have sent the message from the X mobile app or web client, in which case `userId` would be `null`. Always check `lastMessage.authorId` against your account link IDs. Keep your account links cached so you can make this comparison quickly. ### Calculate response time ```typescript theme={null} async function calculateResponseTime(threadId: string) { // Cache account link IDs to determine message direction const { data: accountLinks } = await client.get('/account-links'); const accountLinkIds = new Set(accountLinks.map(a => a.id)); const { data } = await client.get(`/threads/${threadId}/messages`, { params: { limit: 100 } }); const responseTimes: number[] = []; for (let i = 0; i < data.messages.length - 1; i++) { const current = data.messages[i]; const previous = data.messages[i + 1]; const currentIsOurs = accountLinkIds.has(current.authorId); const previousIsOurs = accountLinkIds.has(previous.authorId); // Our reply to a prospect message if (currentIsOurs && !previousIsOurs) { const responseTime = new Date(current.createdAt).getTime() - new Date(previous.createdAt).getTime(); responseTimes.push(responseTime); } } if (responseTimes.length === 0) return null; const avgMs = responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length; const avgMinutes = Math.round(avgMs / 1000 / 60); console.log(`Average response time: ${avgMinutes} minutes`); return avgMinutes; } ``` ### Bulk send messages ```typescript theme={null} async function bulkSend(threadIds: string[], content: string) { const results = []; for (const threadId of threadIds) { try { const { data } = await client.post(`/threads/${threadId}/messages`, { content }); results.push({ threadId, success: true, messageId: data.id }); } catch (error) { results.push({ threadId, success: false, error }); } // Add a small delay between sends to avoid hitting rate limits await new Promise(r => setTimeout(r, 500)); } const sent = results.filter(r => r.success).length; console.log(`Sent ${sent}/${threadIds.length} messages`); return results; } ``` Rate limits are per team across all endpoints. Add delays between bulk sends and implement retry logic with exponential backoff when you receive a `429` response. See [Rate limits](/reference/rate-limits) for current limits and backoff strategies. ## Related endpoints POST /threads//messages POST /threads/messages GET /threads//messages Thread operations and filtering # Working with prospects Source: https://docs.inboxapp.com/guides/prospects Managing prospect data and context in the Inboxapp API ## Overview Prospects represent external users on messaging platforms. This guide covers retrieving prospect data, updating context, and using prospect information for lead management. ## Prospect data model The prospect object contains profile data synced from the platform, plus a `context` object with your team's custom data. ```json theme={null} { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "hzcai5t59nn9vsck3rbuepyg", "documentId": 858224163, "displayName": "Jordan Rivera", "username": "jordanrivera", "handle": "@jordanrivera", "image": "https://pbs.twimg.com/profile_images/.../photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/.../photo.jpg", "bio": "Head of Growth at TechCo. Building the future of SaaS.", "location": "San Francisco, CA", "profileUrl": "https://x.com/jordanrivera", "websiteUrl": "https://techco.com", "websiteDomain": "techco.com", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12400, "followingCount": 892, "postCount": 3420, "engagementCount": 8100, "listedCount": 18, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2024-07-09T14:08:49.000Z", "lastUpdatedAt": "2025-01-15T10:30:00.000Z", "lastEnrichedAt": "2025-01-15T10:30:00.000Z", "lastActiveAt": "2025-01-14T12:00:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.85, "platformData": { "isVerifiedBlue": false, "isVerifiedGold": false, "isVerifiedGray": false, "bannerUrl": "https://pbs.twimg.com/profile_banners/...", "tweetsCount": 3420, "favoritesCount": 8100 }, "context": { "tags": ["t8rvk2m5j0xn4wq7ybftcael"], "statusId": "s3km7xj9wq5p2bvnhfdteoly", "valuation": 50000, "notes": "Decision maker at TechCo. Follow up next week.", "threads": [ { "id": "l44e15irdq4db30i77cgphhx", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "lastMessageTimestamp": "2025-01-15T15:30:00.000Z", "createdAt": "2025-01-10T10:00:00.000Z" } ] } } ``` ### Key fields | Field | Description | | ----------------- | ------------------------------------------------------------------------------------- | | `platform` | Always `"twitter"` for now | | `platformId` | The X numeric user ID (string) | | `externalId` | Inboxapp internal ID. `null` if the prospect hasn't been imported to the database yet | | `displayName` | Display name shown on their profile | | `username` | Handle without `@` | | `handle` | Handle with `@` prefix (e.g., `@jordanrivera`) | | `image` | Profile picture URL from platform | | `imageNormalized` | Profile picture URL normalized to highest quality — prefer this for display | | `bio` | Profile bio/description text | | `verified` | `"none"`, `"verified"` (blue check), `"business"` (gold), or `"government"` (gray) | | `profileType` | `"personal"`, `"business"`, or `"government"` | | `followerCount` | Number of followers (singular, not `followersCount`) | | `confidence` | Data accuracy score from `0` to `1` — higher is more reliable | ### Context fields The `context` object contains your team's custom data for this prospect: | Field | Type | Description | | ----------- | ---------------- | -------------------------------------------------- | | `tags` | `string[]` | Array of tag IDs applied to this prospect | | `statusId` | `string \| null` | Pipeline status ID | | `valuation` | `number \| null` | Deal value | | `notes` | `string \| null` | Free-text notes | | `threads` | `array \| null` | Threads across all account links for this prospect | Tags, status, valuation, and notes are stored on prospects. Assignee and done/not-done technically live on **threads** — see [Assigning and Marking Done](#assigning-and-marking-done) for details. ## Finding prospects ### Lookup by platform ID Find a prospect using their X user ID: ```typescript lookup.ts theme={null} const { data } = await client.get("/prospects/lookup", { params: { platformId: "1876543210987654321", }, }); if (data) { console.log(`Found: @${data.username}`); console.log(`Followers: ${data.followerCount}`); } else { console.log("Prospect not found"); } ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/prospects/lookup?platformId=1876543210987654321" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` Returns the prospect with context directly, or `null` if not found. Not all prospects are searchable via the API. Lookup is only guaranteed to return a prospect if your team has interacted with them (e.g., they messaged you or you started a conversation). New users who haven't interacted with anyone on the platform may not be found. We're working on improving this. ### Get by Inbox ID Retrieve a specific prospect by their Inbox ID: ```typescript theme={null} const { data } = await client.get(`/prospects/${prospectId}`); console.log(`Prospect: @${data.username}`); console.log(`Tags: ${data.context.tags.length}`); ``` Returns the prospect with context directly. Prospects are automatically created when they message you or when you start a conversation with them. ## Updating prospect context Update tags, status, valuation, and notes on a prospect. You can also update `assigneeId` and `done` — these are applied to all threads belonging to this prospect on your team. ### Setting notes and valuation ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { notes: "Interested in Enterprise plan. Follow up next week.", valuation: 50000, }); ``` ### Applying tags Tags are updated incrementally using `addTags` and `removeTags`: ```typescript theme={null} // Add tags await client.patch(`/prospects/${prospectId}/context`, { addTags: ["t8rvk2m5j0xn4wq7ybftcael", "u9swl3n6k1yo5xr8zcgudbfm"], }); // Remove a tag await client.patch(`/prospects/${prospectId}/context`, { removeTags: ["t8rvk2m5j0xn4wq7ybftcael"], }); // Add and remove in one request await client.patch(`/prospects/${prospectId}/context`, { addTags: ["v0txm4o7l2zp6ys9adhvecgn"], removeTags: ["u9swl3n6k1yo5xr8zcgudbfm"], }); ``` There is no `tagIds` field. Tags are always modified incrementally with `addTags` and `removeTags`. ### Setting status ```typescript theme={null} // Get status IDs first const { data: statuses } = await client.get("/statuses"); const qualifiedStatus = statuses.find((s) => s.name === "Qualified"); // Set prospect status await client.patch(`/prospects/${prospectId}/context`, { statusId: qualifiedStatus.id, }); // Clear status await client.patch(`/prospects/${prospectId}/context`, { statusId: null, }); ``` ### Assigning and marking done ```typescript theme={null} // Assign a prospect to a team member await client.patch(`/prospects/${prospectId}/context`, { assigneeId: "r3km7xj9wq5p2bvnhfdteoly", }); // Mark a prospect as done await client.patch(`/prospects/${prospectId}/context`, { done: true, }); // Unassign and reopen await client.patch(`/prospects/${prospectId}/context`, { assigneeId: null, done: false, }); ``` `assigneeId` and `done` technically live on threads, not on the prospect itself. When you set these via the prospect update endpoint, the change is applied to **all threads** for that prospect on your team. If you have multiple account links talking to the same prospect, every thread with that prospect will be assigned or marked done. Keep this in mind for multi-account setups. ### Complete update Multiple context fields can be updated in a single request: ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { notes: "Decision maker at Acme Corp", valuation: 75000, addTags: ["t8rvk2m5j0xn4wq7ybftcael"], statusId: "s4ln8yk0xr6q3cw1behsifup", }); ``` ## Prospect lookup vs thread lookup Two lookup patterns exist for different use cases: | Use Case | Endpoint | Returns | | ------------------------------ | ----------------------- | --------------------------------- | | Get prospect profile + context | `GET /prospects/lookup` | Prospect with context (or `null`) | | Find conversation thread | `GET /threads/lookup` | Thread object (or `null`) | ```typescript theme={null} // When you need prospect profile data const prospect = await client.get("/prospects/lookup", { params: { platformId: "1876543210987654321" }, }); // When you need the conversation const thread = await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", }, }); ``` ## Common workflows ### Lead scoring Calculate a score based on prospect profile: ```typescript theme={null} function calculateLeadScore(prospect: any): number { let score = 0; // Follower count scoring if (prospect.followerCount > 100000) score += 30; else if (prospect.followerCount > 10000) score += 20; else if (prospect.followerCount > 1000) score += 10; // Verification bonus if (prospect.verified !== "none") score += 15; // Bio keywords const bio = prospect.bio?.toLowerCase() || ""; const keywords = ["founder", "ceo", "director", "head of", "vp"]; if (keywords.some((k) => bio.includes(k))) score += 25; // Has website if (prospect.websiteUrl) score += 10; return score; } // Usage const { data: prospect } = await client.get(`/prospects/${prospectId}`); const score = calculateLeadScore(prospect); console.log(`Lead score: ${score}/100`); // Auto-tag based on score if (score >= 70) { await client.patch(`/prospects/${prospect.externalId}/context`, { addTags: [highValueTagId], }); } ``` ### Finding prospects by tag Prospects aren't directly searchable — search threads instead, which include the prospect: ```typescript theme={null} // Get all threads with "Enterprise" tag const { data } = await client.get("/threads", { params: { inbox: "default", "filters[tags][selectedIds][0]": enterpriseTagId, }, }); // Extract prospect info from threads const enterpriseProspects = data.threads.map((thread) => ({ externalId: thread.prospect.externalId, username: thread.prospect.username, followerCount: thread.prospect.followerCount, threadId: thread.id, })); console.log(`${enterpriseProspects.length} active enterprise conversations`); ``` ### Exporting prospect data Export prospects to CSV format via threads: ```typescript theme={null} async function exportProspects() { const allThreads = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get("/threads", { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }), }, }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); // Build CSV rows const rows = allThreads.map((t) => ({ externalId: t.prospect.externalId, platformId: t.prospect.platformId, username: t.prospect.username, displayName: t.prospect.displayName, bio: t.prospect.bio || "", followerCount: t.prospect.followerCount, verified: t.prospect.verified, tags: t.prospect.context.tags.join("; "), statusId: t.prospect.context.statusId || "", valuation: t.prospect.context.valuation || "", notes: t.prospect.context.notes || "", })); return rows; } ``` ## Platform profile data Profile data is synced from X and includes: | Field | Description | Updates | | --------------- | ------------------- | ------------------- | | `username` | @handle | On each interaction | | `displayName` | Display name | On each interaction | | `bio` | Profile bio | On each interaction | | `followerCount` | Followers | Periodically | | `verified` | Verification status | Periodically | | `image` | Avatar URL | On each interaction | Profile data may be slightly stale. Follower counts and other stats are updated periodically, not in real-time. Check `isFresh`, `isStale`, and `confidence` to assess data quality. ## Best practices Use the notes field for context that doesn't fit structured fields: ```typescript theme={null} await client.patch(`/prospects/${id}/context`, { notes: `Meeting 1/15: Discussed pricing. Need to follow up with case study. Key decision maker. Budget approved Q1.` }); ``` Set valuation to prioritize high-value prospects: ```typescript theme={null} const sorted = threads .filter(t => t.prospect.context.valuation) .sort((a, b) => (b.prospect.context.valuation ?? 0) - (a.prospect.context.valuation ?? 0) ); ``` `assigneeId` and `done` technically live on threads, not on the prospect. Tags, status, valuation, and notes live on the prospect. When you set `assigneeId` or `done` via the prospect update endpoint, it applies to **all threads** for that prospect on your team — so if you have multiple accounts talking to the same prospect, all of those threads are affected. For guidance on when to use tags vs statuses, see [Tags & statuses](/guides/tags-and-statuses). ## Next steps GET /prospects/lookup PATCH /prospects//context Organize with tags and pipeline stages Work with conversations # Tags & statuses Source: https://docs.inboxapp.com/guides/tags-and-statuses Organizing prospects with tags and pipeline statuses Tags and statuses help organize prospects in your Inboxapp. Tags are flexible labels for categorization. Statuses represent pipeline stages. | Feature | Tags | Statuses | | ------------ | ------------------------ | ----------------------- | | Per prospect | Multiple | One | | Use case | Flexible labels | Pipeline stages | | Example | "Hot Lead", "Enterprise" | "New → Qualified → Won" | ## Tags ### List tags ```typescript list-tags.ts theme={null} const { data: tags } = await client.get("/tags"); console.log(`Found ${tags.length} tags`); tags.forEach((tag) => { console.log(`${tag.name} (${tag.color})`); }); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/tags" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} [ { "id": "t8rvk2m5j0xn4wq7ybftcael", "name": "Hot Lead", "color": "#ef4444" }, { "id": "u9swl3n6k1yo5xr8zcgudbfm", "name": "Enterprise", "color": "#3b82f6" }, { "id": "v0txm4o7l2zp6ys9adhvecgn", "name": "Partner", "color": "#22c55e" } ] ``` ### Create tag ```typescript theme={null} const { data: tag } = await client.post("/tags", { name: "VIP", color: "purple", }); console.log("Created tag:", tag.id); ``` Colors accept either a **predefined name** (`"red"`, `"blue"`, `"green"`, `"purple"`, etc.) or a **hex code** (`"#ef4444"`). Use `GET /tags/colors` to see all predefined options. ### Update tag ```typescript theme={null} const { data: tag } = await client.patch(`/tags/${tagId}`, { name: "VIP Customer", color: "#6366f1", }); ``` ### Delete tag ```typescript theme={null} await client.delete(`/tags/${tagId}`); ``` Deleting a tag removes it from all prospects. This cannot be undone. ### Apply tags to a prospect Tags are updated incrementally using `addTags` and `removeTags` on the prospect context endpoint: ```typescript theme={null} // Add tags await client.patch(`/prospects/${prospectId}/context`, { addTags: ["t8rvk2m5j0xn4wq7ybftcael", "u9swl3n6k1yo5xr8zcgudbfm"], }); // Remove a tag await client.patch(`/prospects/${prospectId}/context`, { removeTags: ["t8rvk2m5j0xn4wq7ybftcael"], }); // Add and remove in one request await client.patch(`/prospects/${prospectId}/context`, { addTags: ["v0txm4o7l2zp6ys9adhvecgn"], removeTags: ["u9swl3n6k1yo5xr8zcgudbfm"], }); ``` There is no `tagIds` field. Tags are always modified incrementally with `addTags` and `removeTags`. *** ## Statuses ### List statuses ```typescript list-statuses.ts theme={null} const { data: statuses } = await client.get("/statuses"); statuses.forEach((status) => { console.log(`${status.name} (${status.color})`); }); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/statuses" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} [ { "id": "s3km7xj9wq5p2bvnhfdteoly", "name": "New", "color": "#64748b", "createdAt": "2024-06-01T12:00:00.000Z" }, { "id": "s4ln8yk0xr6q3cw1behsifup", "name": "Qualified", "color": "#f59e0b", "createdAt": "2024-06-01T12:00:00.000Z" }, { "id": "s5mo9zl1ys7r4dx2cfitjgvq", "name": "Won", "color": "#22c55e", "createdAt": "2024-06-01T12:00:00.000Z" } ] ``` ### Create status ```typescript theme={null} const { data: status } = await client.post("/statuses", { name: "Demo Scheduled", color: "teal", }); console.log("Created status:", status.id); ``` ### Update status ```typescript theme={null} const { data: status } = await client.patch(`/statuses/${statusId}`, { name: "Demo Complete", color: "#14b8a6", }); ``` ### Delete status ```typescript theme={null} await client.delete(`/statuses/${statusId}`); ``` Deleting a status removes it from all prospects. This cannot be undone. The consumer is responsible for handling any side effects — the API does not emit individual `prospect.statusChanged` events for each affected prospect. ### Set prospect status ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { statusId: "s4ln8yk0xr6q3cw1behsifup", }); ``` ### Clear prospect status ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { statusId: null, }); ``` *** ## Filtering threads by tags and statuses Filter threads using the `filters` parameter on `GET /threads`. Filters combine with AND logic: ```typescript theme={null} const { data } = await client.get("/threads", { params: { inbox: "default", "filters[tags][selectedIds][0]": "t8rvk2m5j0xn4wq7ybftcael", "filters[statuses][selectedIds][0]": "s4ln8yk0xr6q3cw1behsifup", "filters[assignees][noAssignee]": true, }, }); ``` Use `filters[tags][noTags]: true` or `filters[statuses][noStatus]: true` to find threads with no tags or no status respectively. See [Managing threads — Filtering](/guides/managing-threads#filtering-threads) for the full list of filter options and the [Query parameters reference](/reference/query-parameters) for encoding details. *** ## Common patterns ### Tag system setup ```typescript theme={null} const standardTags = [ { name: "Hot Lead", color: "red" }, { name: "Warm Lead", color: "orange" }, { name: "Cold Lead", color: "slate" }, { name: "Customer", color: "green" }, { name: "Partner", color: "blue" }, { name: "Do Not Contact", color: "#1f2937" }, ]; for (const tag of standardTags) { try { await client.post("/tags", tag); console.log(`Created: ${tag.name}`); } catch (error) { if (error.response?.status === 409) { console.log(`Exists: ${tag.name}`); } else { throw error; } } } ``` ### Pipeline setup ```typescript theme={null} const pipeline = [ { name: "New", color: "slate" }, { name: "Contacted", color: "blue" }, { name: "Qualified", color: "amber" }, { name: "Demo", color: "purple" }, { name: "Proposal", color: "pink" }, { name: "Won", color: "green" }, { name: "Lost", color: "red" }, ]; for (const status of pipeline) { try { await client.post("/statuses", status); console.log(`Created: ${status.name}`); } catch (error) { if (error.response?.status === 409) { console.log(`Exists: ${status.name}`); } } } ``` ### Auto-tagging by follower count ```typescript theme={null} async function autoTag(prospectId: string, followerCount: number) { const tagsToAdd: string[] = []; if (followerCount > 100000) tagsToAdd.push(influencerTagId); else if (followerCount > 10000) tagsToAdd.push(highReachTagId); if (tagsToAdd.length > 0) { await client.patch(`/prospects/${prospectId}/context`, { addTags: tagsToAdd, }); } } ``` ## Related endpoints GET /tags POST /tags GET /statuses POST /statuses PATCH /prospects//context GET /colors # Help Center Source: https://docs.inboxapp.com/help Guides and answers for Inboxapp. Find answers for setting up Inboxapp, managing leads, running campaigns, and more. Set up Inboxapp, link your X account, and finish the onboarding checklist. Link X accounts, share inboxes, create teams, and manage pipeline statuses. Import, filter, export, and understand reachable vs unreachable leads. Create and run DM campaigns on Pro: sequences, personalization, scheduling, stages, and troubleshooting. How to get replies on X, stay within platform rules, and choose who to message. Plans, credits, invoices, and subscription management. Fix sync problems, delivery icons, and common errors. Use the Inboxapp API to manage inboxes and messages programmatically. Still need help? Start a chat with the button in the bottom-right corner, or email [support@inboxapp.com](mailto:support@inboxapp.com). # Account & teams Source: https://docs.inboxapp.com/help/account Link X accounts, invite teammates, and configure pipeline statuses. Install the Chrome extension, open X, and link your account with the Inboxapp floating icon. Invite a teammate to your Inboxapp team so they can access linked X inboxes. Create a separate team workspace for another brand, client, or billing group. Create, reorder, edit, and delete CRM pipeline statuses in Settings. Create a pay-per-use X developer app and connect it to Inboxapp for a higher daily DM allowance. # Add an X account to Inboxapp Source: https://docs.inboxapp.com/help/account/how-to-add-an-x-twitter-account-to-inboxapp Install the Chrome extension, open X, and link your account with the Inboxapp floating icon. ## 1. Sign up or log in Go to [inboxapp.com](https://inboxapp.com) and create an account, or log in. ## 2. Install the Chrome extension Install the [Inboxapp Chrome extension](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe) from the Chrome Web Store. ## 3. Open X Go to [x.com](https://x.com) and make sure you’re logged into the account you want to connect. ## 4. Link the account Inboxapp floating icon menu on X with a Link button next to the account 1. Find the **Inboxapp floating icon** on x.com. 2. Click it and select the account to link. 3. Click **Link**. ## 5. Wait for sync Inboxapp starts syncing automatically. When sync finishes, the account is available in your workspace. See [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect). If you use XChat, you’ll be asked for your **XChat PIN** — [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform). ## Related * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) * [Chrome extension troubleshooting](/help/troubleshooting/chrome-extension-troubleshooting) * [Share your X inbox with a team member](/help/account/share-your-x-twitter-inbox-with-a-team-member) Still stuck? Contact support from the app. # Add statuses to the pipeline Source: https://docs.inboxapp.com/help/account/how-to-add-statuses-to-the-pipeline Create, reorder, edit, and delete CRM pipeline statuses in Settings. Customize the stages of your CRM pipeline. 1. Log in, select your team, and open **Settings**. Settings entry point in Inboxapp 2. Open **Statuses** to see your current stages. Statuses list in Settings 3. Click **Add Status**, name it, and pick a color. Add Status form with name and color 4. Drag and drop statuses to change their order in the pipeline. Drag handles for reordering statuses 5. Use the **⋯** menu on a status to edit or delete it. Status overflow menu with edit and delete ## Related * [Personalize Inboxapp](/help/getting-started-with-inboxapp/personalize-inboxapp-to-your-needs) * [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) Still stuck? Contact support from the app. # Create a new team Source: https://docs.inboxapp.com/help/account/how-to-create-a-new-team Create a separate team workspace for another brand, client, or billing group. Use a new team when you need a separate workspace for another brand, client, or billing group. 1. Hover over your **team name** at the top of the screen. 2. Click **Create team**. 3. Enter a name for the team. 4. Select a plan — **Inboxapp** (\$25/month or \$250/year) or **Pro** (\$129/month per connected X account). See [Plans and credits](/help/billing/billing-plans-and-credits). 5. Confirm to create the team. Separate teams keep members, linked accounts, and settings isolated per client or brand. ## Related * [Share your X inbox with a team member](/help/account/share-your-x-twitter-inbox-with-a-team-member) * [Plans and credits](/help/billing/billing-plans-and-credits) Still stuck? Contact support from the app. # Set up your X API integration Source: https://docs.inboxapp.com/help/account/setting-up-your-x-api-integration Create a pay-per-use X developer app and connect it to Inboxapp for a higher daily DM allowance. Connecting your own X developer app (BYOK) raises your daily DM allowance to whatever your X API tier allows — [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp). You pay X directly for API usage. ## 1. Set up your X developer account Sign in to the X account that should own the developer account, then go to [console.x.com](https://console.x.com). Accept the developer agreement and enter your use case if you haven’t already. ## 2. Create a pay-per-use app 1. Open **Apps** in the left sidebar and select the **Pay Per Use** category. 2. Click **Create App**. 3. Name the app, set the environment to **Production**, and click **Create New Client Application**. 4. Click **Close** — you don’t need the consumer key, secret key, or bearer token. ## 3. Set user authentication settings Refresh the page, open your new app, and go to **Keys & Tokens** → **User authentication settings** → **Set up**. | Section | Value | | --------------- | --------------------------------- | | App permissions | **Read and write Direct message** | | Type of App | **Web App, Automated App or Bot** | | Callback URI | Copy it from Inboxapp | | Website URL | Any URL (required by X, unused) | Click **Save Changes**, then copy the **Client ID** and **Client Secret** into Inboxapp. Those two values authenticate the integration. ## 4. Purchase credits In the X console, go to **Billing** → **Credits** and buy a prepaid amount. Turn on **auto recharge** so sending doesn’t stop mid-campaign. ## 5. Authorize your accounts Back in Inboxapp, authorize each X account individually, then enable the integration. ## Related * [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [Getting started with the Inboxapp API](/help/api/getting-started-with-the-inboxapp-api-a-complete-guide) Still stuck? Contact support from the app. # Share your X inbox with a team member Source: https://docs.inboxapp.com/help/account/share-your-x-twitter-inbox-with-a-team-member Invite a teammate to your Inboxapp team so they can access linked X inboxes. 1. Log in at [inboxapp.com](https://inboxapp.com). 2. Switch to the correct **team** (team name at the top). Team switcher showing the active team name 3. Open **Settings → General**. 4. Click **Invite**, enter their email, and click **Send Invites**. Team members section with Invite button 5. They accept the email invite and join the team. **Note:** Invited members currently get access to all linked accounts on the team. Finer-grained permissions are not available yet. ## Related * [Create a new team](/help/account/how-to-create-a-new-team) * [How pricing works for multiple accounts](/help/billing/how-does-the-pricing-work-for-multiple-accounts) Still stuck? Contact support from the app. # API Source: https://docs.inboxapp.com/help/api Use the Inboxapp API to manage inboxes and messages programmatically. Get an API key and call the Inboxapp API. The full reference lives in the API docs. # Inboxapp API quickstart Source: https://docs.inboxapp.com/help/api/getting-started-with-the-inboxapp-api-a-complete-guide Get an API key and call the Inboxapp API. The full reference lives in the API docs. Use the Inboxapp API to manage inboxes and messages from your own apps. Full reference: [API overview](/developer-api) · [Quickstart](/quickstart). ## Prerequisites * An Inboxapp account * An **API key** from account settings (treat it like a password) * Optional: Postman or cURL for testing ## Base URL `https://inboxapp.com/api/v1` Authenticate with a Bearer token using your API key in the `Authorization` header. ## Common tasks Follow the official guides for listing inboxes, working with threads, and sending messages: * [Quickstart](/quickstart) * [Messages](/guides/messages) **Note:** Replying to existing conversations works on all plans. Sending the **first message** to a new contact needs the outbound/API add-on described in Billing — [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Plans and credits](/help/billing/billing-plans-and-credits) * [API collection](/help/api) Still stuck? Contact support from the app, or email [support@inboxapp.com](mailto:support@inboxapp.com) for API issues. # Billing Source: https://docs.inboxapp.com/help/billing Plans, credits, invoices, and subscription management. Inboxapp and Pro pricing, what each plan includes, and how credits work on Pro. How Inboxapp and Pro are billed when you connect more than one X account or invite teammates. Open the Stripe billing portal from Settings → Billing to view and download invoices. Cancel from Settings → Billing via the Stripe portal, and what happens to access until period end. Access through the end of the billing period, invoices, data, and how to resubscribe. # Plans and credits Source: https://docs.inboxapp.com/help/billing/billing-plans-and-credits Inboxapp and Pro pricing, what each plan includes, and how credits work on Pro. Inboxapp has two plans today: **Inboxapp** for managing conversations, and **Pro** for sales and lead generation. Credits apply on **Pro**. ## Plans ### Inboxapp **\$25 per month**, or **\$250 per year**. Built for managing conversations and customer relationships. **Includes:** * Unified inbox * CRM * Unlimited tags and statuses * 1 pipeline with up to 5 stages * API access * Webhooks The Inboxapp plan does **not** include monthly credits. ### Pro **\$129 per month per connected X account**. Built for sales teams and outbound lead generation. This plan may change as we introduce a future Grow offering for campaigns; Pro remains available for sales use cases today. **Includes everything in the Inboxapp plan, plus:** * Lead lists and lead database * CSV exports * Multiple linked X accounts * Campaigns (sales outreach) * **100,000 monthly credits** per connected X account ## Free trial Every new workspace starts with a **7-day free trial**, including **500 trial credits**. These features need an active paid subscription: * Campaigns * CSV exports * Credit packs * Advanced API access (first-message outbound) ## Credits (Pro) Credits are used when Inboxapp creates or enriches **new** lead or profile records. ### Rule **1 usable lead or profile = 1 credit** A usable profile is one Inboxapp successfully imports into your workspace. Unavailable, suspended, deleted, or failed profiles do not consume credits. ### What uses credits | Action | Credits | | ----------------------------------- | -------------------------- | | Scrape followers | 1 per new follower | | Scrape following | 1 per new profile | | Scrape tweet likes | 1 per new like | | Scrape retweets | 1 per new retweet | | Scrape quote tweets | 1 per new profile | | Scrape replies | 1 per new reply | | Scrape X list members | 1 per new member | | Scrape X list followers | 1 per new follower | | Upload a CSV or text lead list | 1 per new imported profile | | Add usernames or profiles to a list | 1 per new imported profile | ### What does not use credits * Replying to and managing conversations * Tags, statuses, notes, custom fields, pipeline updates * Leads already in your workspace * Duplicate profiles in the same import * Suspended, deleted, failed, or unavailable profiles ### How imports are charged Large imports reserve credits before processing. When the import finishes, Inboxapp refunds credits for existing leads, duplicates, failed rows, and unusable profiles. Only **new usable profiles** are charged. **Example:** CSV with 2,000 rows → 150 existing + 100 duplicates + 50 unavailable = **1,700 credits** charged. ### Credit usage order 1. Bonus credits 2. Monthly included credits 3. Prepaid credit packs 4. Credit overage (if enabled) ### Monthly credits Pro includes **100,000 credits per connected X account** each billing cycle. The Inboxapp plan does not include monthly credits. ### Credit overage **\$0.005 per credit** In **Settings → Billing** you can enable or disable overage and set a spending limit. If overage is off and you run out of credits, new credit-consuming jobs pause until credits are available again. ### Prepaid credit packs Available on Pro: | Pack | Price | Cost per credit | | ---- | ------- | --------------- | | 25K | \$100 | \$0.004 | | 100K | \$300 | \$0.003 | | 500K | \$1,250 | \$0.0025 | | 2M | \$4,000 | \$0.002 | ## Advanced API access **\$199/month** optional add-on: lets API integrations send the **first message** to new prospects. Replying to existing conversations through the API is already included. ## FAQ **When are credits charged?** Only when Inboxapp imports or enriches a new usable lead or profile. **Do replies use credits?** No. **Are duplicates charged?** No. Existing and duplicate profiles are excluded from final usage. **What if I run out of credits?** With overage on, jobs continue at \$0.005/credit. With overage off, jobs pause until you have credits again. **Can I buy more credits?** Yes, on an active Pro workspace via prepaid packs. ## Related * [How pricing works for multiple accounts](/help/billing/how-does-the-pricing-work-for-multiple-accounts) * [Where to see your invoices](/help/billing/where-can-i-see-my-invoices) * [Cancel your subscription](/help/billing/how-to-cancel-my-subscription) Still stuck? Contact support from the app. # How pricing works for multiple accounts Source: https://docs.inboxapp.com/help/billing/how-does-the-pricing-work-for-multiple-accounts How Inboxapp and Pro are billed when you connect more than one X account or invite teammates. ## Plans at a glance * **Inboxapp:** **\$25/month** or **\$250/year** — unified inbox and CRM. * **Pro:** **\$129/month per connected X account** — sales and lead generation, including credits. Full feature and credit details: [Plans and credits](/help/billing/billing-plans-and-credits). ## Multiple X accounts On **Pro**, each additional connected X account is billed at **\$129/month**. Charges are prorated for the rest of the current billing period. The **Inboxapp** plan is priced per workspace subscription (\$25/month or \$250/year). Check **Settings → Billing** for what your workspace currently allows for linked accounts. ## Team members Pro supports collaborating with teammates on shared inboxes. Seat availability and any seat charges show in **Settings → Billing** for your workspace. ## Related * [Plans and credits](/help/billing/billing-plans-and-credits) * [Create a new team](/help/account/how-to-create-a-new-team) * [Share your X inbox with a team member](/help/account/share-your-x-twitter-inbox-with-a-team-member) Still stuck? Contact support from the app. # Cancel your subscription Source: https://docs.inboxapp.com/help/billing/how-to-cancel-my-subscription Cancel from Settings → Billing via the Stripe portal, and what happens to access until period end. You can cancel anytime through Stripe from Inboxapp billing settings. 1. Log in to **Inboxapp**. 2. Go to **Settings → Billing**. 3. Click **Open Billing** to open the Stripe billing portal. 4. In **Subscription**, click **Cancel plan**. 5. Confirm the cancellation. Your plan stays active until the end of the current billing period. You are not charged again after that. ## Related * [What happens after you cancel](/help/billing/what-happens-after-you-cancel) * [Plans and credits](/help/billing/billing-plans-and-credits) * [Where to see your invoices](/help/billing/where-can-i-see-my-invoices) Still stuck? Contact support from the app. # What happens after you cancel Source: https://docs.inboxapp.com/help/billing/what-happens-after-you-cancel Access through the end of the billing period, invoices, data, and how to resubscribe. After you cancel in Stripe, your subscription stays active until the **end of the current billing period**. You are not charged again after that. ## During the rest of the period * You keep using Inboxapp on your current plan until the period ends. * Invoices for past charges remain available in the Stripe portal — [Where to see your invoices](/help/billing/where-can-i-see-my-invoices). ## After the period ends * Paid features tied to your plan stop according to Billing (for example Pro-only tools and credit packs). * Your workspace data (conversations, leads, settings) remains available for recovery or export needs — contact support if you need a full export after access changes. * You can resubscribe anytime from **Settings → Billing**. Cancel steps: [Cancel your subscription](/help/billing/how-to-cancel-my-subscription). ## Related * [Cancel your subscription](/help/billing/how-to-cancel-my-subscription) * [Plans and credits](/help/billing/billing-plans-and-credits) * [Export leads](/help/leads-management/organize/exporting-leads-from-inboxapp) Still stuck? Contact support from the app. # Where to see your invoices Source: https://docs.inboxapp.com/help/billing/where-can-i-see-my-invoices Open the Stripe billing portal from Settings → Billing to view and download invoices. Past invoices live in the Stripe billing portal. 1. Log in to **Inboxapp**. 2. Go to **Settings → Billing**. 3. Click **Open Billing**. 4. Scroll to **Invoice**. 5. Click **Download** next to any invoice. ## Related * [Plans and credits](/help/billing/billing-plans-and-credits) * [Cancel your subscription](/help/billing/how-to-cancel-my-subscription) Still stuck? Contact support from the app. # Campaigns Source: https://docs.inboxapp.com/help/campaigns Create and run DM campaigns on Pro: sequences, personalization, scheduling, stages, and troubleshooting. Create campaigns, sequences, variables, scheduling, and A/B tests. Campaign stages, replies, and analytics. Daily DM caps and avoiding duplicate outreach. Campaign not sending and other common issues. Replies, platform rules, and who to message. # Build Source: https://docs.inboxapp.com/help/campaigns/build Create campaigns, sequences, variables, scheduling, and A/B tests. End-to-end: pick a lead list, write a DM, set schedule options, launch, and check stages. Build a campaign with follow-up steps and delays between messages. Insert lead fields such as first name into campaign messages with the custom variable control. Set send times so Inboxapp adjusts delivery for recipient time zones. Inboxapp has no native in-campaign A/B test — split lists and run parallel campaigns instead. Personalize, sequence thoughtfully, respect limits and time zones, and segment before you scale. # DM outreach best practices Source: https://docs.inboxapp.com/help/campaigns/build/best-practices-for-dm-outreach-campaigns Personalize, sequence thoughtfully, respect limits and time zones, and segment before you scale. Practical habits that improve reply rates and keep outreach within X’s limits. ## Before you launch * Credible X profile and a clear goal (replies, meetings, or another metric) * A segmented lead list — [Filtering and segmenting](/help/leads-management/organize/filtering-and-segmenting-your-leads) * Exclude **Contacted** when you don’t want duplicates — [Prevent duplicate messaging](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted) ## Message quality * Personalize with a specific detail (bio, recent post, shared context) and variables — [Custom variables](/help/campaigns/build/personalizing-dms-with-custom-variables) * Keep the first DM short; lead with value, not a hard sell * Avoid dumping links in the opening message ## Sequences Use a short multi-step flow with days between steps — [Multi-step sequences](/help/campaigns/build/how-to-design-multi-step-dm-sequences): 1. Intro + value 2. Soft follow-up 3. Final attempt with a clear ask or resource ## Delivery and etiquette * Stay under [daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * Ramp volume up over one to two weeks instead of opening at your full allowance — [Ramp up gradually](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp#ramp-up-gradually) * Schedule for recipients’ daytime — [Scheduling](/help/campaigns/build/scheduling-campaigns-across-time-zones) * Warm up with organic engagement when you can (likes, replies) before cold DMs ## Related * [How to get replies on X](/help/outreach/how-to-get-replies-on-x) * [Stay within X rules](/help/outreach/stay-within-x-rules-for-outreach) * [Who to message and when](/help/outreach/who-to-message-and-when) * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Campaign analytics](/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates) Still stuck? Contact support from the app. # Create your first campaign Source: https://docs.inboxapp.com/help/campaigns/build/create-your-first-campaign End-to-end: pick a lead list, write a DM, set schedule options, launch, and check stages. Campaigns send DMs from a linked X account to a lead list. You need **Pro** and a connected X account — [Plans and credits](/help/billing/billing-plans-and-credits). ## Before you start * [Link an X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [Create a lead list](/help/leads-management/organize/create-your-first-lead-list) with reachable leads * Optionally exclude **Contacted** so you don’t re-message people — [Prevent duplicate messaging](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted) ## Steps 1. Open **Campaigns** → **New Campaign**. 2. Name the campaign and choose your **lead list**. 3. Under **Excluded Lists**, add **Contacted** if you want to skip anyone you’ve already messaged. 4. Write your first DM. Insert variables (for example first name) from the custom variable control — [Personalize with variables](/help/campaigns/build/personalizing-dms-with-custom-variables). 5. Optional: **Add Step** for follow-ups with delays — [Multi-step sequences](/help/campaigns/build/how-to-design-multi-step-dm-sequences). 6. Set send window / timezone options if needed — [Scheduling across time zones](/help/campaigns/build/scheduling-campaigns-across-time-zones). 7. Review and **Launch Campaign**. ## After launch * Watch **Stages** (Pending, In-Sequence, Replied, and others) — [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) * Check **Overview** for reply and completion rates — [Campaign analytics](/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates) * If nothing sends: [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) ## Related * [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * [What happens when a lead replies](/help/campaigns/run-and-measure/what-happens-when-a-lead-replies-to-my-campaign) * [Campaign best practices](/help/campaigns/build/best-practices-for-dm-outreach-campaigns) Still stuck? Contact support from the app. # Design multi-step DM sequences Source: https://docs.inboxapp.com/help/campaigns/build/how-to-design-multi-step-dm-sequences Build a campaign with follow-up steps and delays between messages. Use multi-step sequences to send a first DM, then follow-ups after a delay if the lead hasn’t replied. 1. Confirm your X account is linked. Connected account indicator in Inboxapp 2. Open **Campaigns** → **New Campaign**. Campaigns sidebar with New Campaign 3. Choose the recipient **lead list**. 4. Write the first message. 5. Click **Add Step**, set a delay (for example 1 day), and write each follow-up. 6. Add as many steps as you need. 7. Review timing and copy, then **Launch Campaign**. Launch Campaign confirmation When someone replies, the sequence stops for that lead — [What happens when a lead replies](/help/campaigns/run-and-measure/what-happens-when-a-lead-replies-to-my-campaign). ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Personalize with variables](/help/campaigns/build/personalizing-dms-with-custom-variables) * [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) Still stuck? Contact support from the app. # Run A/B tests with segmented lists Source: https://docs.inboxapp.com/help/campaigns/build/how-to-run-a-b-tests-in-campaigns Inboxapp has no native in-campaign A/B test — split lists and run parallel campaigns instead. There is no native A/B test inside a single campaign. To compare copy, split the audience into similar lists and run parallel campaigns. ## Steps 1. Split one audience into equal lists (for example List A and List B, \~same size). 2. Create **Campaign A** → List A and **Campaign B** → List B. 3. Keep every setting the same except **one** variable (opening line, CTA, tone, or personalization). 4. Launch both at the same time. 5. Compare reply rate and other Overview metrics — [Campaign analytics](/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates). ## Tips * Even splits and identical settings keep the test fair * Run long enough for meaningful reply counts * Roll the winner into future campaigns ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Filtering and segmenting leads](/help/leads-management/organize/filtering-and-segmenting-your-leads) Still stuck? Contact support from the app. # Personalize DMs with custom variables Source: https://docs.inboxapp.com/help/campaigns/build/personalizing-dms-with-custom-variables Insert lead fields such as first name into campaign messages with the custom variable control. Custom variables fill lead fields into each DM so messages aren’t identical. 1. Open **Campaigns** and create or edit a campaign. 2. Name the campaign, then continue. Insert variable control in message editor 3. Select the recipient list and continue. Variable picker list 4. In the message editor, open the **Custom Variable** control (often shown as an asterisk or similar). Message preview with variable placeholders 5. Choose a field such as **First Name**. Inboxapp substitutes each lead’s value at send time. Personalization mapping for lead fields Insert variables from the picker — don’t type placeholder syntax by hand, or personalization may fail. ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Multi-step sequences](/help/campaigns/build/how-to-design-multi-step-dm-sequences) * [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) Still stuck? Contact support from the app. # Schedule campaigns across time zones Source: https://docs.inboxapp.com/help/campaigns/build/scheduling-campaigns-across-time-zones Set send times so Inboxapp adjusts delivery for recipient time zones. Schedule sends in your timezone; Inboxapp adjusts for recipients where possible. 1. Confirm your X account is linked. 2. Open **Campaigns** → **New Campaign** (or edit an existing draft). Campaign schedule timezone setting 3. Choose your lead list. Send window configuration 4. Set the send window / schedule. Prefer recipients’ daytime hours. Timezone preview for recipients 5. Review the summary, then launch. ## Tips * Aim for morning or afternoon in the recipient’s local time. * Keep daily limits in mind — [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp). ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) Still stuck? Contact support from the app. # Fix Source: https://docs.inboxapp.com/help/campaigns/fix Campaign not sending and other common issues. Check excluded lists, reachable filtering, account link, schedule, and daily DM limits. What “We’re currently filtering out prospects that can’t be targeted with this campaign” means. Fixes for filtering, stuck campaigns, personalization, time zones, and delivery icons. # Common campaign issues Source: https://docs.inboxapp.com/help/campaigns/fix/common-campaign-issues-and-how-to-fix-them Fixes for filtering, stuck campaigns, personalization, time zones, and delivery icons. ## Filtering banner / empty audience If you see **“We’re currently filtering out prospects that can’t be targeted”**, excluded lists and unreachable leads were removed. * Review **Excluded Lists** * Start with a larger audience * Import fresher reachable leads — [Reachable vs unreachable](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Details: [Filtering banner](/help/campaigns/fix/what-does-we-re-currently-filtering-out-prospects-that-can-t-be-targeted-with-this-campaign-mean) · [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) ## Campaign feels stuck Check **Campaigns → \[campaign] → Stages** (Pending, In-Sequence, Replied, Unresponsive, Failed). * X account still linked * Daily DM limits — [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * Schedule / time zones — [Scheduling](/help/campaigns/build/scheduling-campaigns-across-time-zones) * Sequence delays not excessively long ## Personalization not working Insert variables with the **Custom Variable** control — don’t type placeholders by hand — [Personalize with variables](/help/campaigns/build/personalizing-dms-with-custom-variables). ## Odd send times Set the schedule in your timezone and confirm on the summary before launch — [Scheduling](/help/campaigns/build/scheduling-campaigns-across-time-zones). ## Undelivered or failed icons Older encrypted messages may show icons after X encryption changes — [Undelivered](/help/troubleshooting/why-do-some-of-my-messages-show-an-undelivered-icon) · [Failed to send](/help/troubleshooting/why-do-some-of-my-messages-show-a-failed-to-send-icon). ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) Still stuck? Contact support from the app. # Campaign filtering banner Source: https://docs.inboxapp.com/help/campaigns/fix/what-does-we-re-currently-filtering-out-prospects-that-can-t-be-targeted-with-this-campaign-mean What “We’re currently filtering out prospects that can’t be targeted with this campaign” means. When you see **“We’re currently filtering out prospects that can’t be targeted with this campaign,”** Inboxapp is keeping only reachable leads in the send. ## What’s happening 1. **Exclusion check** — prospects on your excluded lists are removed. 2. **DM eligibility** — profiles that can’t receive DMs are removed. No action is required for the filter itself. The campaign continues with the remaining reachable leads. If more leads are removed than you expect, review excluded lists, start with a larger audience, or import a fresher list. Full rules: [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads). If nothing is sending: [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages). ## Related * [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) * [Common campaign issues](/help/campaigns/fix/common-campaign-issues-and-how-to-fix-them) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Campaign not sending Source: https://docs.inboxapp.com/help/campaigns/fix/why-is-my-campaign-not-sending-messages Check excluded lists, reachable filtering, account link, schedule, and daily DM limits. If a campaign isn’t sending, work through these causes. ## 1. Filtering removed the audience Before send, Inboxapp: 1. Removes **excluded lists** (for example **Contacted**) 2. Removes **unreachable** leads (closed DMs, private, suspended, and similar) Campaign stages view showing pending or filtered leads **Check:** excluded lists aren’t wiping the audience; starting list is large enough after filtering — [Reachable vs unreachable](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads), [Filtering banner](/help/campaigns/fix/what-does-we-re-currently-filtering-out-prospects-that-can-t-be-targeted-with-this-campaign-mean). ## 2. Account or schedule * X account still linked — [Add an X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * Workspace timezone / send window configured — [Scheduling](/help/campaigns/build/scheduling-campaigns-across-time-zones) ## 3. Daily limit reached New-contact sends pause until the next day when you hit the plan cap — [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp). ## Checklist * Review excluded lists and remaining audience size * Confirm account link and schedule * Check daily usage * Import fresher reachable leads if needed More fixes: [Common campaign issues](/help/campaigns/fix/common-campaign-issues-and-how-to-fix-them). ## Related * [Common campaign issues](/help/campaigns/fix/common-campaign-issues-and-how-to-fix-them) * [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) Still stuck? Contact support from the app. # Limits and safety Source: https://docs.inboxapp.com/help/campaigns/limits-and-safety Daily DM caps and avoiding duplicate outreach. How many new-contact DMs you can send per day on Pro, how BYOK changes the limit, and how to ramp up. Exclude the Contacted system list in campaign settings so prior outreach isn’t duplicated. Authenticity, spam policy, and habits that keep accounts healthy. # Daily DM limits Source: https://docs.inboxapp.com/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp How many new-contact DMs you can send per day on Pro, how BYOK changes the limit, and how to ramp up. Requires **Pro** (campaigns are Pro-only). Daily limits apply to **new contacts** (people you haven’t messaged before): | Setup | New-contact DMs per day | | -------------------------------------- | --------------------------------------------------- | | **Pro** (Inboxapp connection) | Up to 500 per connected X account | | **Pro** with your own X API key (BYOK) | Higher — set by your own X developer app’s API tier | | Custom / enterprise | Higher limits by agreement | ## Details * **Follow-ups don’t count** toward the new-contact daily limit — sequence follow-ups can continue after you hit the new-DM cap. * The **500/day** allowance is **per connected X account**, not per workspace. * With your **own X API key (BYOK)**, sending runs through your own X developer app, so each account’s allowance is whatever your X API tier allows — [Set up your X API integration](/help/account/setting-up-your-x-api-integration). * Confirm your plan in [Plans and credits](/help/billing/billing-plans-and-credits). ## Ramp up gradually Treat your allowance as a ceiling, not a target. Start at a few DMs per hour and raise volume over one to two weeks — on BYOK too, where the allowance is higher. Conservative starting points, not thresholds published by X: | Stage | New-contact DMs per hour | New-contact DMs per day | | ------------ | ---------------------------------------- | ----------------------- | | Days 1–3 | 5–10 | 20–30 | | Days 4–7 | 10–20 | 50–100 | | Week 2 | 20–40 | 100–200 | | Steady state | Increase only while replies stay healthy | Up to your allowance | To control the pace: * Launch smaller lead lists and add leads in batches. * Spread sends across a send window — [Scheduling across time zones](/help/campaigns/build/scheduling-campaigns-across-time-zones). * Hold each increase for a few days and watch your reply rate. * Step back down if sends show as undelivered or failed — [Undelivered](/help/troubleshooting/why-do-some-of-my-messages-show-an-undelivered-icon) · [Failed to send](/help/troubleshooting/why-do-some-of-my-messages-show-a-failed-to-send-icon). ## Related * [Set up your X API integration](/help/account/setting-up-your-x-api-integration) * [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) * [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) * [Stay within X rules](/help/outreach/stay-within-x-rules-for-outreach) * [X’s rules and policies](https://help.x.com/en/rules-and-policies) (X’s own docs) Still stuck? Contact support from the app. # Prevent messaging leads you’ve already contacted Source: https://docs.inboxapp.com/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted Exclude the Contacted system list in campaign settings so prior outreach isn’t duplicated. Requires **Pro**. Exclude the **Contacted** system list on a campaign so prior outreach isn’t duplicated. What Contacted contains: [How the Contacted list prevents duplicates](/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging). ## Exclude Contacted 1. Create or edit a campaign. 2. Open **Campaign Settings**. 3. Under **Excluded Lists**, select **Contacted**. 4. Save. Campaign settings showing excluded Contacted list At launch, Inboxapp removes excluded lists first, then filters unreachable DMs — [Reachable vs unreachable](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads). ## Related * [How the Contacted list prevents duplicates](/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging) * [Campaign not sending](/help/campaigns/fix/why-is-my-campaign-not-sending-messages) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Run and measure Source: https://docs.inboxapp.com/help/campaigns/run-and-measure Campaign stages, replies, and analytics. Pending, In-Sequence, Replied, Unresponsive, and Failed — how stages update automatically. Replies stop the sequence for that lead and move them to the Replied stage automatically. Find targeted count, replies, reply rate, completion rate, and duration on the Overview tab. # Campaign analytics Source: https://docs.inboxapp.com/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates Find targeted count, replies, reply rate, completion rate, and duration on the Overview tab. Use **Overview** to review how a campaign performed. 1. Open **Campaigns** and select the campaign (often after it has completed). 2. Open the campaign. Campaigns list with a completed campaign 3. Open the **Overview** tab. Campaign Overview tab selected 4. Review targeted contacts, replies, reply rate, completion rate, recent conversations, and any sentiment summary shown. Overview metrics for replies and completion rate 5. Check completion duration on the overview when available. For per-lead status, use **Stages** — [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp). ## Related * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [A/B tests with segmented lists](/help/campaigns/build/how-to-run-a-b-tests-in-campaigns) Still stuck? Contact support from the app. # Campaign stages Source: https://docs.inboxapp.com/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp Pending, In-Sequence, Replied, Unresponsive, and Failed — how stages update automatically. Campaign **Stages** show where each lead sits. Stages update automatically — you can’t drag leads between them. 1. Open **Campaigns** and select a campaign. Campaign Stages tab overview 2. Open the **Stages** tab (alongside Overview and Settings). Stage counts for pending and replied 3. Review counts and drill into a stage for individual leads. Stage detail for a lead ## Stage meanings | Stage | Meaning | | ---------------- | ---------------------------------------- | | **Pending** | Not yet in the active send flow | | **In-Sequence** | Receiving or waiting on sequence steps | | **Replied** | Lead replied — sequence stopped for them | | **Unresponsive** | Finished sequence without a reply | | **Failed** | Send failed for that lead | ## Related * [What happens when a lead replies](/help/campaigns/run-and-measure/what-happens-when-a-lead-replies-to-my-campaign) * [Campaign analytics](/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates) * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) Still stuck? Contact support from the app. # What happens when a lead replies Source: https://docs.inboxapp.com/help/campaigns/run-and-measure/what-happens-when-a-lead-replies-to-my-campaign Replies stop the sequence for that lead and move them to the Replied stage automatically. When a lead replies to a campaign message: * They leave the active sequence — no further automated follow-ups * They move to the **Replied** stage * You continue the conversation in the inbox Stages update automatically; you can’t move leads between stages by hand — [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp). ## Related * [Campaign stages](/help/campaigns/run-and-measure/understanding-campaign-stages-in-inboxapp) * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) Still stuck? Contact support from the app. # Get started Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp Set up Inboxapp, link X, and complete your first workspace checklist. Sign up, install the Chrome extension, link X, and start using your workspace. Customize statuses, tags, currency, and snippets from Settings. What to expect after you install the Chrome extension and link X — sync speed, Contacted/Mutual timing, and tips. XChat is X’s encrypted DM experience. Link your account and enter your XChat PIN so Inboxapp can sync messages. Checklist to link X, configure settings, import leads, and verify sync before you go live. Short definitions for Inboxapp terms: Inboxapp plan, Pro, credits, Contacted, XChat, reachable leads, and more. # Onboarding checklist Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/comprehensive-onboarding-setup-checklist-getting-started-with-inboxapp Checklist to link X, configure settings, import leads, and verify sync before you go live. Use this checklist after signup. Linked articles have the full steps. ## Setup * [ ] Install the Chrome extension and [link your X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [ ] Enter your [XChat PIN](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform) if prompted * [ ] Confirm [DM sync](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect) is running * [ ] [Personalize](/help/getting-started-with-inboxapp/personalize-inboxapp-to-your-needs) statuses, tags, currency, and snippets * [ ] Invite teammates if needed — [Share your X inbox](/help/account/share-your-x-twitter-inbox-with-a-team-member) ## Leads * [ ] [Create a lead list](/help/leads-management/organize/create-your-first-lead-list) * [ ] Import via [CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file), [followers](/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account), or another source * [ ] Apply [tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) * [ ] Understand [reachable vs unreachable](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) filtering ## Billing * [ ] Review [Plans and credits](/help/billing/billing-plans-and-credits) and pick Inboxapp or Pro when ready ## Campaigns (Pro) * [ ] [Create your first campaign](/help/campaigns/build/create-your-first-campaign) * [ ] Exclude **Contacted** if you don’t want duplicates — [Prevent duplicate messaging](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted) ## Related * [Glossary](/help/getting-started-with-inboxapp/glossary) * [Set up your Inboxapp account](/help/getting-started-with-inboxapp/setup-your-inboxapp-account) Still stuck? Contact support from the app. # Glossary Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/glossary Short definitions for Inboxapp terms: Inboxapp plan, Pro, credits, Contacted, XChat, reachable leads, and more. | Term | Meaning | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **Inboxapp plan** | \$25/month or \$250/year — unified inbox and CRM. [Plans and credits](/help/billing/billing-plans-and-credits) | | **Pro plan** | \$129/month per connected X account — sales and lead gen, includes credits | | **Credits** | 1 credit = 1 new usable lead/profile imported or enriched on Pro | | **Lead list** | A collection of prospects you import into and message from | | **System lists** | Automatic lists: **Contacted** and **Mutual** — [System lists](/help/leads-management/concepts/understanding-system-lists) | | **Contacted** | Everyone you’ve messaged across linked X accounts | | **Mutual** | People you follow who also follow you | | **Reachable** | A profile that can receive DMs from you | | **Unreachable** | A profile X will not deliver DMs to — filtered automatically | | **XChat** | X’s encrypted DM experience; needs a PIN to sync — [XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform) | | **Linked account** | An X account connected to your Inboxapp workspace via the Chrome extension | | **Pipeline / statuses** | CRM stages for conversations — [Add statuses](/help/account/how-to-add-statuses-to-the-pipeline) | | **Tags** | Labels you create to organize leads and conversations — [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) | | **Custom fields** | Extra data fields on leads beyond the defaults | | **Campaigns** | Outbound DM sequences on **Pro** — start at [Create your first campaign](/help/campaigns/build/create-your-first-campaign) | | **Post engagement import** | Import likers, retweeters, repliers, or quote-tweeters from a post | | **Excluded Lists** | Campaign setting to skip lists such as **Contacted** | | **Campaign stages** | Pending, In-Sequence, Replied, Unresponsive, Failed | ## Related * [Set up your Inboxapp account](/help/getting-started-with-inboxapp/setup-your-inboxapp-account) * [Plans and credits](/help/billing/billing-plans-and-credits) # How Inboxapp syncs your DMs Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect What to expect after you install the Chrome extension and link X — sync speed, Contacted/Mutual timing, and tips. After you install the Chrome extension and link X, Inboxapp syncs your DMs in the background. ## 1. Install the extension and link X Install the Inboxapp Chrome extension, then link your X account. Details: [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp). ## 2. Sync timing * **Small inboxes** (under \~100 contacts) usually sync quickly. * **Larger inboxes** can take a few hours. ## 3. Use Inboxapp while syncing You can read, reply, and organize messages while sync continues. ## 4. Contacted and Mutual lists **Contacted** and **Mutual** system lists are not ready until the full sync finishes. They load last. See [System lists](/help/leads-management/concepts/understanding-system-lists). ## Tips * You can close the browser; sync continues in the cloud. * You can keep using Inboxapp during sync. ## Related * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) * [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) Still stuck? Contact support from the app. # Personalize Inboxapp Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/personalize-inboxapp-to-your-needs Customize statuses, tags, currency, and snippets from Settings. Customize your workspace from **Settings**. ## Statuses In **Settings → Statuses** you can create, edit, remove, and drag-to-reorder pipeline stages. Details: [Add statuses to the pipeline](/help/account/how-to-add-statuses-to-the-pipeline). ## Tags In **Settings → Tags**, create tags (with colors), edit, or delete them. Step-by-step: [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields). ## Currency and valuation In **Settings → Prospects**, choose the currency used for prospect valuation in the inbox and pipeline. ## Snippets In **Settings → Snippets**, create reusable message templates. In chat, type `;` to insert a snippet. ## Related * [Add statuses to the pipeline](/help/account/how-to-add-statuses-to-the-pipeline) * [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) * [Set up your Inboxapp account](/help/getting-started-with-inboxapp/setup-your-inboxapp-account) Still stuck? Contact support from the app. # Set up your Inboxapp account Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/setup-your-inboxapp-account Sign up, install the Chrome extension, link X, and start using your workspace. Set up takes a few minutes. 1. **Sign up** at [inboxapp.com](https://inboxapp.com). 2. **Install the Chrome extension** from the [Chrome Web Store](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe). 3. **Link your X account** when prompted (details: [Add an X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp)). 4. Explore the inbox, tags, and statuses. Personalize defaults in [Personalize Inboxapp](/help/getting-started-with-inboxapp/personalize-inboxapp-to-your-needs). 5. Choose a plan when you’re ready — see [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform) * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) * [Onboarding checklist](/help/getting-started-with-inboxapp/comprehensive-onboarding-setup-checklist-getting-started-with-inboxapp) Still stuck? Contact support from the app. # What is XChat and how to use it with Inboxapp Source: https://docs.inboxapp.com/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform XChat is X’s encrypted DM experience. Link your account and enter your XChat PIN so Inboxapp can sync messages. XChat is X’s encrypted direct messaging experience. Inboxapp supports XChat so your conversations can sync into your workspace. ## Link your X account 1. Go to **Linked Accounts**. 2. Select **Connect X Account** and complete authentication. 3. When prompted, enter your **XChat PIN**. The PIN lets Inboxapp sync your XChat conversations. Your PIN and messages are encrypted on our servers. ## Why the PIN is required X requires the PIN for third-party apps to access XChat messages. Without it, Inboxapp cannot retrieve or sync those conversations. ## Security * Messages and PIN codes are encrypted on our servers. * Only you can access synced messages through your workspace. * We do not store your PIN in plain text. ## Related * [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) * [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect) Still stuck? Contact support from the app. # Leads Source: https://docs.inboxapp.com/help/leads-management Import, organize, filter, and export leads — including reachable vs unreachable. Ways to bring leads into Inboxapp, and how credits apply on Pro. Lists, tags, filters, and exports for your leads. System lists, Contacted, and reachable vs unreachable leads. # Concepts Source: https://docs.inboxapp.com/help/leads-management/concepts System lists, Contacted, and reachable vs unreachable leads. Contacted and Mutual are automatic lists Inboxapp maintains from your messaging history and follows. Why some leads can’t receive DMs, how Inboxapp filters them, and what to do when too many are removed. Contacted is a system list of everyone you’ve messaged; campaigns use it to avoid messaging the same person twice. # How the Contacted list prevents duplicates Source: https://docs.inboxapp.com/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging Contacted is a system list of everyone you’ve messaged; campaigns use it to avoid messaging the same person twice. The **Contacted** system list is everyone you’ve ever messaged across linked X accounts, including history synced into Inboxapp. Campaigns use it so the same person isn’t messaged again by accident. ## How it works When you launch a campaign, Inboxapp typically: 1. Removes prospects on your **excluded lists** 2. Removes people already on **Contacted** This runs automatically when **Contacted** is excluded. To exclude it in campaign settings: [Prevent messaging leads you’ve already contacted](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted). ## System lists | List | Who’s on it | Purpose | | ------------- | ----------------------------------------------- | -------------------------- | | **Contacted** | Everyone you’ve messaged across linked accounts | Prevent duplicate outreach | | **Mutual** | People you follow who follow you back | Network overlap | Contacted and Mutual aren’t ready until DM sync finishes — [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect). More on both lists: [System lists](/help/leads-management/concepts/understanding-system-lists). ## Related * [Prevent messaging leads you’ve already contacted](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted) * [System lists](/help/leads-management/concepts/understanding-system-lists) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Reachable vs unreachable leads Source: https://docs.inboxapp.com/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads Why some leads can’t receive DMs, how Inboxapp filters them, and what to do when too many are removed. Leads fall into two buckets: **reachable** (can receive DMs) and **unreachable** (X will not deliver). Inboxapp filters unreachable profiles so you don’t waste sends. ## Reachable Reachable leads can receive your DMs. Typically they: * Have open or allowed messaging settings * Allow DMs from non-followers, or already follow you * Are active and in good standing on X ## Unreachable Unreachable leads are filtered out because X won’t deliver DMs. Common cases: * Inactive, deleted, or suspended accounts * Private accounts that restrict incoming messages * Users who disabled DMs or opted out * Closed or restricted DM settings (including verified-only DMs) ## How filtering works Before a campaign send (and when evaluating imports), Inboxapp runs checks such as: 1. **Excluded lists** — remove anyone on lists you chose to exclude 2. **Reachability** — remove profiles that can’t receive DMs This is automatic. You don’t need to remove unreachable leads by hand. ## If too many leads are filtered * Import fresher or more active audiences * Review your excluded lists * Target profiles more likely to have open DMs * Make sure your starting list is large enough after filtering ## Related * [System lists](/help/leads-management/concepts/understanding-system-lists) * [How the Contacted list prevents duplicates](/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging) * [Campaign filtering banner](/help/campaigns/fix/what-does-we-re-currently-filtering-out-prospects-that-can-t-be-targeted-with-this-campaign-mean) Still stuck? Contact support from the app. # System lists Source: https://docs.inboxapp.com/help/leads-management/concepts/understanding-system-lists Contacted and Mutual are automatic lists Inboxapp maintains from your messaging history and follows. System lists are managed automatically by Inboxapp. There are two today: ## Contacted Everyone you’ve ever messaged across all linked X accounts. * If someone has multiple accounts, messages from each are included. * Past outreach history synced into Inboxapp is included as well. ## Mutual People who follow you and whom you follow back. ## Related * [How the Contacted list prevents duplicate messaging](/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging) * [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect) Still stuck? Contact support from the app. # Import Source: https://docs.inboxapp.com/help/leads-management/importing Bring leads into Inboxapp from CSV, followers, lists, and post engagement. Upload a CSV of X usernames or user IDs into a lead list, with copyable examples. Import recent followers of a public X account into a lead list. Import accounts that a public X profile follows into a lead list. Import members or followers of an X list into a lead list. Import profiles that liked, retweeted, replied to, or quote-tweeted a post into a lead list. Fixes for CSV column errors, follower import limits, filtering, and slow imports. # Import leads from post engagement Source: https://docs.inboxapp.com/help/leads-management/importing/importing-leads-from-post-engagement Import profiles that liked, retweeted, replied to, or quote-tweeted a post into a lead list. You can build a lead list from people who engaged with a post on X. ## Steps 1. Open a lead list (or [create one](/help/leads-management/organize/create-your-first-lead-list)). 2. Click **Add Leads**. 3. Choose the engagement import for the interaction you want: * **Likes** — typically your own posts * **Retweets** * **Replies** * **Quote tweets** 4. Paste the post URL (or select the post when prompted). 5. Start the import. Import runs in the background. Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). Unreachable or invalid profiles are filtered and are not charged — [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads). ## Related * [Create your first lead list](/help/leads-management/organize/create-your-first-lead-list) * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Troubleshooting lead imports](/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide) Still stuck? Contact support from the app. # Import leads from an X list Source: https://docs.inboxapp.com/help/leads-management/importing/importing-leads-from-twitter-lists-members-and-followers Import members or followers of an X list into a lead list. Import people from an X list into Inboxapp. 1. Open a lead list (or [create one](/help/leads-management/organize/create-your-first-lead-list)). 2. Click **Add Leads**. 3. Choose the X list import option. 4. Pick whether to import **list members** or **list followers**. Import dialog for an X list 5. Enter the list details and start the import. Members vs followers import choice for an X list Import runs in the background. Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Import leads from followers](/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account) * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Troubleshooting lead imports](/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide) Still stuck? Contact support from the app. # Import leads from a CSV Source: https://docs.inboxapp.com/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file Upload a CSV of X usernames or user IDs into a lead list, with copyable examples. Import an existing list of leads with a CSV. ## Steps 1. Open a lead list (or [create one](/help/leads-management/organize/create-your-first-lead-list)). 2. Click **Add Leads**. 3. Choose **Upload a CSV**. Your CSV must include **one** of these columns: | Column | Meaning | Example | | ----------------- | ------------------------------ | ------------------------ | | `username` | X handle (with or without `@`) | `elonmusk`, `@inboxapp_` | | `id` or `user_ID` | Numeric X user ID | `44196397` | User IDs are often more reliable when your export tool provides them. ## CSV examples Save either example below as a `.csv` file. ```csv username-import.csv theme={null} username KevinPicchi inboxapp_ ``` ```csv id-import.csv theme={null} id 1171040852623417351 1566123362161725440 ``` ## After upload 1. Inboxapp analyzes the file and shows how many valid usernames or IDs it found. 2. Click **Import** to confirm. 3. Import continues in the background — you can leave the page. CSV upload preview showing detected usernames ready to import Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Troubleshooting lead imports](/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide) * [Export leads](/help/leads-management/organize/exporting-leads-from-inboxapp) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Import leads from followers Source: https://docs.inboxapp.com/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account Import recent followers of a public X account into a lead list. Import people who follow a public X account into one of your lead lists. 1. Open a lead list (or [create one](/help/leads-management/organize/create-your-first-lead-list)). 2. Click **Add Leads**. 3. Choose **Import from Followers**. 4. Enter the X handle (for example `@inboxapp_`). 5. Click **Start Import**. Import runs in the background. Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Import leads from following](/help/leads-management/importing/importing-leads-to-inboxapp-from-the-following-of-an-x-twitter-account) * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Troubleshooting lead imports](/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide) Still stuck? Contact support from the app. # Import leads from following Source: https://docs.inboxapp.com/help/leads-management/importing/importing-leads-to-inboxapp-from-the-following-of-an-x-twitter-account Import accounts that a public X profile follows into a lead list. Import the accounts a public X profile follows into a lead list. 1. Open a lead list (or [create one](/help/leads-management/organize/create-your-first-lead-list)). 2. Click **Add Leads**. 3. Choose **Import from Following**. 4. Enter the X handle (for example `@inboxapp_`). 5. Click **Start Import**. Import runs in the background. Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). ## Related * [Import leads from followers](/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account) * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Troubleshooting lead imports](/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide) Still stuck? Contact support from the app. # Troubleshooting lead imports Source: https://docs.inboxapp.com/help/leads-management/importing/troubleshooting-lead-imports-csv-followers-quick-guide Fixes for CSV column errors, follower import limits, filtering, and slow imports. | Problem | Fix | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CSV missing `username` or `id` / `user_ID` | Add a required column. See [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) | | File too large or messy | Import smaller batches | | Follower import smaller than expected | Check plan limits in **Settings → Billing** | | Many leads removed after import | Unreachable profiles are filtered — [Reachable vs unreachable](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) | | Previously contacted leads removed | Expected when excluded lists / Contacted protection apply — [Contacted list](/help/leads-management/concepts/how-the-contacted-list-protects-you-from-duplicate-messaging) | | Import seems slow | Imports run in the background; refresh the list after a few minutes | ## Related * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Import leads from followers](/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Organize Source: https://docs.inboxapp.com/help/leads-management/organize Lists, tags, filters, and exports for your leads. Create a lead list, then add people from search, CSV, followers, or other import sources. Create tags and custom fields to organize leads, then filter and segment with them. Use tags, custom fields, bio search, and export filters to organize and target leads. Export filtered or selected leads to a file from the Leads page. # Create your first lead list Source: https://docs.inboxapp.com/help/leads-management/organize/create-your-first-lead-list Create a lead list, then add people from search, CSV, followers, or other import sources. ## 1. Create a list 1. Go to **Leads**. 2. Click **Create New List**. 3. Name the list (for example, “Investors”). 4. Click **Save**. ## 2. Choose an import source From **Add Leads** you can import via: * Inboxapp’s X prospecting database (search) * CSV upload — [Import from CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * Followers of an X account — [Import from followers](/help/leads-management/importing/importing-leads-to-inboxapp-from-followers-of-an-x-twitter-account) * Following of an X account — [Import from following](/help/leads-management/importing/importing-leads-to-inboxapp-from-the-following-of-an-x-twitter-account) * X lists — [Import from X lists](/help/leads-management/importing/importing-leads-from-twitter-lists-members-and-followers) * Plain text of usernames or IDs * Engagement on a post (likes, retweets, replies, quote tweets) — [Import from post engagement](/help/leads-management/importing/importing-leads-from-post-engagement) Credits apply on **Pro** — see [Plans and credits](/help/billing/billing-plans-and-credits). ## 3. Example: add from database search 1. Choose **Search Leads**. 2. Filter by keyword — for example **Bio → Contains → investor**. 3. Select profiles (drag to select), then click **Add to List** and choose your list. Lead search results with bio filter for investor Add to List control for selected search results ## Related * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) * [Reachable vs unreachable leads](/help/leads-management/concepts/understanding-reachable-vs-unreachable-leads) Still stuck? Contact support from the app. # Export leads Source: https://docs.inboxapp.com/help/leads-management/organize/exporting-leads-from-inboxapp Export filtered or selected leads to a file from the Leads page. Export leads for backup, reporting, or use in another tool. You need **Admin** or **Export** permission. 1. Open **Leads**. Leads page with list of contacts 2. Optionally filter by date, status, tag, source, or custom field. Lead filters applied before export 3. Select specific leads, or leave selection empty to export the filtered set. Selected leads in the table ready to export 4. Click **Export**. Export button on the leads toolbar 5. Download the file when ready (large exports may finish in the background under notifications). ## Troubleshooting | Problem | Fix | | -------------------------------- | ---------------------------------------------------- | | No Export button | Ask an admin for Export or Admin permission | | Custom fields missing | Include them in the export options before generating | | Export fails or is huge | Export smaller filtered batches | | Dates look wrong in Excel/Sheets | Set the column format (for example YYYY-MM-DD) | ## Related * [Import leads from a CSV](/help/leads-management/importing/importing-leads-to-inboxapp-from-a-csv-file) * [Filtering and segmenting leads](/help/leads-management/organize/filtering-and-segmenting-your-leads) Still stuck? Contact support from the app. # Filtering and segmenting leads Source: https://docs.inboxapp.com/help/leads-management/organize/filtering-and-segmenting-your-leads Use tags, custom fields, bio search, and export filters to organize and target leads. Organize leads with tags and custom fields, then filter for outreach or export. ## Tags and custom fields Create and manage them in Settings — step-by-step: [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields). ## Bio search (database) In **Search Leads**, use filters such as **Bio → Contains** to find profiles in Inboxapp’s prospecting database (for example, “investor”). Bio filtering applies to database profiles, not already-imported leads. ## Segment ideas Group by interest, engagement, role, or industry, then tailor messaging per segment. ## Export filters When exporting, filter by date, status, tag, source, or custom field instead of exporting everything — [Export leads](/help/leads-management/organize/exporting-leads-from-inboxapp). ## Related * [Tags and custom fields](/help/leads-management/organize/tags-and-custom-fields) * [Create your first lead list](/help/leads-management/organize/create-your-first-lead-list) * [System lists](/help/leads-management/concepts/understanding-system-lists) Still stuck? Contact support from the app. # Tags and custom fields Source: https://docs.inboxapp.com/help/leads-management/organize/tags-and-custom-fields Create tags and custom fields to organize leads, then filter and segment with them. Use tags and custom fields to organize leads beyond the defaults. ## Tags 1. Go to **Settings → Tags**. 2. Create, edit, or delete tags. 3. Assign colors if you want quick visual cues. Common uses: source, priority, interest, client, or campaign cohort. You can apply tags to prospects and conversations. ## Custom fields 1. Go to **Settings** (custom fields section for your workspace). 2. Add fields you need — for example Lead Source or Interest Level. 3. Fill those fields on leads as you work. Pipeline **statuses** are configured under **Settings → Statuses** — [Add statuses to the pipeline](/help/account/how-to-add-statuses-to-the-pipeline). ## Filtering and segmentation With tags and fields in place you can: * Segment lists by interest, engagement, role, or industry * Filter exports by date, status, tag, source, or custom field — [Export leads](/help/leads-management/organize/exporting-leads-from-inboxapp) * Search the prospecting database with filters such as **Bio → Contains** (database profiles only) More detail: [Filtering and segmenting leads](/help/leads-management/organize/filtering-and-segmenting-your-leads). ## Related * [Filtering and segmenting leads](/help/leads-management/organize/filtering-and-segmenting-your-leads) * [Create your first lead list](/help/leads-management/organize/create-your-first-lead-list) * [Add statuses to the pipeline](/help/account/how-to-add-statuses-to-the-pipeline) Still stuck? Contact support from the app. # Outreach tips Source: https://docs.inboxapp.com/help/outreach How to get replies on X, stay within platform rules, and choose who to message. Practical guidance for respectful DM outreach on X. Follow platform rules first — reply rates come second. Profile, personalization, short copy, and sequences that earn responses. Limits, authenticity, and habits that keep accounts healthy. ICP, warm vs cold outreach, timing, and when to stop. Product how-tos for campaigns live under [Campaigns](/help/campaigns). # How to get replies on X Source: https://docs.inboxapp.com/help/outreach/how-to-get-replies-on-x Earn DM replies with a credible profile, short personalized copy, and patient sequences — not volume. Reply rate tracks how relevant and respectful you are — not how many messages you fire. Use this as a checklist before you scale. ## Fix the profile first People open the DM, then check who you are. | Element | What to aim for | | ------------ | -------------------------------------------------------------------------------------------------------------- | | Photo | Clear face or recognizable mark — not a logo-only brand avatar when possible | | Bio | Who you help and how, in plain language | | Activity | Recent posts that show a real person | | Verification | Strongly preferred — X often filters messages from unverified accounts into requests or blocks them by default | Personal accounts usually outperform brand accounts for cold outreach. People reply to people. ## Write DMs people finish reading * Keep the first message short — a few sentences max * Open with a specific detail (bio, recent post, shared niche), not a pitch * Ask one easy question, or offer one useful observation * Skip links in the first message unless the prospect asked, or you’re sharing an X post / lead magnet they expect * Avoid walls of text — long identical messages look like spam to people and to X Use variables for name or company fields, but add a human detail by hand — [Custom variables](/help/campaigns/build/personalizing-dms-with-custom-variables). ### Patterns that work | Pattern | Example shape | | ------------------ | ---------------------------------------------------------------- | | Context + question | “Saw your note on — are you still exploring ?” | | Soft value | “We put together a short checklist for dealing with . Want it?” | | Warm follow-up | Reference their reply or post before asking for time | Hard sells and “book a demo” openers usually tank reply rates. ## Sequence without nagging One strong first message beats a noisy sequence. Add steps only after a 1-message campaign already gets replies. Typical shape — [Multi-step sequences](/help/campaigns/build/how-to-design-multi-step-dm-sequences): 1. Intro + specific context 2. Soft follow-up a few days later 3. Final note with a clear ask or resource, then stop Space steps by days, not hours. When someone replies, the sequence should stop — [What happens when a lead replies](/help/campaigns/run-and-measure/what-happens-when-a-lead-replies-to-my-campaign). ## Reply like a human * Answer quickly while the thread is warm * Keep the same tone you used in the opener * Move to a call or link only after they engage * If they’re not interested, thank them and exit ## Measure the right thing Track reply rate and conversation quality before volume — [Campaign analytics](/help/campaigns/run-and-measure/campaign-analytics-completion-and-reply-rates). If replies are low, fix list fit and copy before raising daily sends. ## Related * [Stay within X rules](/help/outreach/stay-within-x-rules-for-outreach) * [Who to message and when](/help/outreach/who-to-message-and-when) * [DM outreach best practices](/help/campaigns/build/best-practices-for-dm-outreach-campaigns) * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) Still stuck? Contact support from the app. # Stay within X rules for outreach Source: https://docs.inboxapp.com/help/outreach/stay-within-x-rules-for-outreach Keep DM outreach authentic, within platform limits, and aligned with X’s spam and automation policies. Sustainable outreach follows [X’s rules](https://help.x.com/en/rules-and-policies) — especially the [Platform manipulation and spam](https://help.x.com/en/rules-and-policies/platform-manipulation) policy. Inboxapp helps you stay inside product limits; you’re still responsible for how you message people. Don’t try to “bypass” X limits, blast identical copy at scale, buy engagement, or scrape data outside authorized channels. Those patterns get accounts restricted. ## What X cares about | Do | Don’t | | --------------------------------------------------- | ----------------------------------------------- | | Authentic accounts with a clear identity | Fake, disposable, or misleading accounts | | Relevant, non-duplicative DMs | Bulk identical or unsolicited spam | | Automation that stays within allowed product limits | Unauthorized scripts or aggressive bot behavior | | Consent-aware follow-ups that stop when ignored | Harassment, relentless chasing, or scam tactics | You’re responsible for third-party apps you authorize on your X account. Use tools that respect platform limits — don’t stack multiple senders to force volume. ## Stay under daily caps X caps how many **new contacts** each account can DM per day. Inboxapp stays within your allowance, whether that’s the standard one or your own developer app’s on BYOK — [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp). Practical habits: * Ramp new or quiet accounts up slowly — start at a few DMs per hour ([suggested ramp](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp#ramp-up-gradually)) * Prefer quality lists over maxing the daily counter every day * Treat the cap as a ceiling, not a target ## Make messages look human * Vary copy across segments — don’t send the same paragraph to thousands of people * Personalize beyond `{firstName}` * Limit follow-ups (two or three max), then stop * Don’t dump tracking links or aggressive CTAs in the opener * Exclude people you’ve already contacted — [Prevent duplicate messaging](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted) ## Account hygiene * Prefer verified accounts for outbound DMs when possible * Keep the Chrome extension and Linked Accounts healthy — [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) * Don’t buy followers, likes, or engagement * Don’t scrape X outside official / authorized data access — use Inboxapp lead tools and imports you have rights to use * One authentic presence beats a farm of lookalike accounts ## If delivery looks wrong Undelivered or failed icons often mean platform or connection issues, not “send harder” — [Undelivered](/help/troubleshooting/why-do-some-of-my-messages-show-an-undelivered-icon) · [Failed to send](/help/troubleshooting/why-do-some-of-my-messages-show-a-failed-to-send-icon). Pause, fix the account, then resume at a conservative pace. ## Related * [How to get replies on X](/help/outreach/how-to-get-replies-on-x) * [Who to message and when](/help/outreach/who-to-message-and-when) * [Daily DM limits](/help/campaigns/limits-and-safety/how-many-dms-can-i-send-per-day-on-x-twitter-with-inboxapp) * [DM outreach best practices](/help/campaigns/build/best-practices-for-dm-outreach-campaigns) Still stuck? Contact support from the app. # Who to message and when Source: https://docs.inboxapp.com/help/outreach/who-to-message-and-when Build tighter lists, prefer warm context, time sends for the recipient, and know when to stop. Most weak campaigns fail on targeting and timing — not tooling. Message fewer, better-fit people when they’re likely to read. ## Define your ICP on X Your ideal customer on X may look different than on LinkedIn. Note: * Keywords and phrases that show up in bios * Creators, communities, or accounts they engage with * Locations, roles, or niches that convert for you * Signals that they’re open to DMs (active posting, public conversation) Segment lists by source and intent so the opener can match — [Filtering and segmenting](/help/leads-management/organize/filtering-and-segmenting-your-leads). ## Prefer warm context over pure cold | Source | Why it works | How to treat it | | ----------------------------------------------- | --------------------------- | -------------------------------------------- | | People who already follow or engage with you | They took a step toward you | Reference shared content; softer ask | | Replies / likes on relevant posts | Fresh context | Mention the post; keep it short | | Lists, followers of niche accounts, CSV imports | Scalable, but colder | Stronger personalization; slower volume | | Prior conversations | Highest trust | Don’t re-pitch blindly — continue the thread | Build lists from imports and research you have rights to use — [Importing leads](/help/leads-management/importing). Exclude **Contacted** when you don’t want repeats — [Prevent duplicate messaging](/help/campaigns/limits-and-safety/how-to-prevent-messaging-leads-you-ve-already-contacted). Skip purchased engagement and scraped dumps of unrelated users. Bad lists burn reply rate and account trust. ## When to send * Schedule for the prospect’s local daytime — [Scheduling across time zones](/help/campaigns/build/scheduling-campaigns-across-time-zones) * Weekday mornings and early business hours usually beat late nights and weekends for B2B * Space follow-ups by days so you don’t stack three messages in one evening If you can, engage lightly on public posts before a cold DM. A recognizable name helps — it isn’t a substitute for a good list. ## When to stop Stop the sequence when: * They reply (positive or not) — handle it in the inbox * They ignore the full sequence * They ask not to be contacted * Delivery keeps failing — fix the account before retrying Persistence past a clear “no” is how outreach turns into harassment. Leave the door open without pushing it. ## Put it together 1. Segment a tight list for one ICP 2. Write one short opener that only that segment would believe 3. Cap daily new contacts well below the platform ceiling until reply rate looks healthy 4. Add a 2–3 step sequence only after the first message works 5. Iterate on list + copy before you scale ## Related * [How to get replies on X](/help/outreach/how-to-get-replies-on-x) * [Stay within X rules](/help/outreach/stay-within-x-rules-for-outreach) * [Create your first lead list](/help/leads-management/organize/create-your-first-lead-list) * [Create your first campaign](/help/campaigns/build/create-your-first-campaign) Still stuck? Contact support from the app. # Troubleshooting Source: https://docs.inboxapp.com/help/troubleshooting Fix sync issues, delivery icons, and common errors. Fix a missing Inboxapp icon, linking failures, and common extension sync problems. Check the Chrome extension, XChat PIN, sync timing, and IndexedDB when DMs don’t appear in Inboxapp. What the red undelivered icon means on older encrypted messages, and why no action is usually required. What the red failed-to-send icon means on encrypted messages, and when to resend or delete. # Chrome extension troubleshooting Source: https://docs.inboxapp.com/help/troubleshooting/chrome-extension-troubleshooting Fix a missing Inboxapp icon, linking failures, and common extension sync problems. Most sync and linking issues start with the Chrome extension. ## Checklist 1. Install the [Inboxapp Chrome extension](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe) from the official Chrome Web Store. 2. On [x.com](https://x.com), confirm you’re logged into the correct X account. 3. Look for the **Inboxapp floating icon**. If it’s missing, remove and reinstall the extension, then refresh x.com. 4. Click the icon → select the account → **Link**. 5. In Inboxapp, open **Linked Accounts** and confirm the account shows as connected. 6. For XChat, enter your **XChat PIN** when asked — [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform). ## Still not syncing? Follow [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes), including the IndexedDB reset if the extension is linked but nothing updates. ## Related * [Add an X account to Inboxapp](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect) * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) Still stuck? Contact support from the app. # Messages not syncing Source: https://docs.inboxapp.com/help/troubleshooting/messages-not-syncing-try-these-fixes Check the Chrome extension, XChat PIN, sync timing, and IndexedDB when DMs don’t appear in Inboxapp. If messages aren’t appearing in Inboxapp, work through these checks. ## 1. Chrome extension * Confirm the [Inboxapp Chrome extension](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe) is installed. * On x.com, use the correct X account. * Confirm the **Inboxapp floating icon** appears. Missing icon → reinstall the extension. * Click the icon → select the account → **Link**, or reconnect under **Linked Accounts**. Full checklist: [Chrome extension troubleshooting](/help/troubleshooting/chrome-extension-troubleshooting). ## 2. Browser basics * Fully quit and reopen Chrome. * Remember large inboxes can take hours; small ones (\<100 contacts) are usually fast. * Sync continues in the cloud — you don’t need to keep the browser open. ## 3. XChat PIN Encrypted XChat conversations need your **XChat PIN**. Without it, those messages can’t sync — [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform). ## 4. Clear IndexedDB (advanced) Use this when the extension is linked but sync stays stuck. 1. Open **x.com**. 2. Right-click → **Inspect**. 3. Open the **Application** tab. 4. Under **Storage → IndexedDB**, delete Inboxapp / messaging entries. 5. Close DevTools and refresh. This clears local browser data only — your messages in Inboxapp remain. ## Related * [How Inboxapp syncs your DMs](/help/getting-started-with-inboxapp/how-inboxapp-syncs-your-dms-and-what-to-expect) * [Add an X account](/help/account/how-to-add-an-x-twitter-account-to-inboxapp) * [Chrome extension troubleshooting](/help/troubleshooting/chrome-extension-troubleshooting) Still stuck? Contact support from the app. # Failed to send icon Source: https://docs.inboxapp.com/help/troubleshooting/why-do-some-of-my-messages-show-a-failed-to-send-icon What the red failed-to-send icon means on encrypted messages, and when to resend or delete. A red **failed to send** icon means that specific message was not accepted for delivery on X’s side (usually during an encryption change). The conversation may also show a red dot. ## What to do | Message type | Action | | ---------------- | -------------------------------------------------------------------------------------------------------- | | Manual message | **Resend** (delivers with current encryption; old failed copy is removed) or **Delete** if it’s obsolete | | Campaign message | No action — affected campaign messages are rescheduled automatically | ## Did you lose messages? No. Messages stay visible in Inboxapp. The icon means the recipient may not have received that one message. If they already replied, they received your outreach. New messages you send go out normally. ## Related * [Undelivered icon](/help/troubleshooting/why-do-some-of-my-messages-show-an-undelivered-icon) * [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform) * [Messages not syncing](/help/troubleshooting/messages-not-syncing-try-these-fixes) Still stuck? Contact support from the app. # Undelivered icon Source: https://docs.inboxapp.com/help/troubleshooting/why-do-some-of-my-messages-show-an-undelivered-icon What the red undelivered icon means on older encrypted messages, and why no action is usually required. A red **undelivered** icon marks an older encrypted message that may not have reached the recipient after X tightened encryption-key checks. **If you did not enable XChat before 21 Jan 2026, you are not affected.** ## What to do No action is required for these older indicators. New messages send with current encryption. Your messages are still visible in Inboxapp. If the other person already replied, they received your messages. ## Related * [Failed to send icon](/help/troubleshooting/why-do-some-of-my-messages-show-a-failed-to-send-icon) * [What is XChat](/help/getting-started-with-inboxapp/what-is-xchat-and-how-use-it-with-our-platform) Still stuck? Contact support from the app. # Get started with Inboxapp Source: https://docs.inboxapp.com/index Choose a path for setting up Inboxapp, sending your first outreach, or connecting the API. Inboxapp helps teams manage conversations, leads, and outreach across X. Start with the path that matches your goal. Connect X and confirm your DMs are syncing. Create a lead list and launch your first campaign. Make your first API call and connect your tools. # MCP server Source: https://docs.inboxapp.com/mcp-server Connect AI tools like Claude Desktop and Cursor to the Inboxapp API The Inboxapp MCP server gives AI tools direct access to the full [REST API](/api-reference/introduction) — threads, messages, prospects, tags, and more. ## Authentication The MCP server uses the same API token as the REST API. Pass it in the `Authorization` header. Need an API token? See [Authentication](/configuration/authentication). ## Server URL ``` https://mcp.inboxapp.com/mcp ``` ## Client setup ### Claude Desktop Add to your config file: * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json claude_desktop_config.json theme={null} { "mcpServers": { "inbox": { "url": "https://mcp.inboxapp.com/mcp", "headers": { "Authorization": "Bearer " } } } } ``` ### Cursor Add to `.cursor/mcp.json` in your project root: ```json .cursor/mcp.json theme={null} { "mcpServers": { "inbox": { "url": "https://mcp.inboxapp.com/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Replace `` with your API token. # Quick start Source: https://docs.inboxapp.com/quickstart Send your first DM in 5 minutes ## Send your first message This tutorial walks you through sending your first DM using the Inboxapp API. You'll learn the core workflow: authenticate, find an account, locate or create a thread, and send a message. Before starting, make sure you have an API token. See the [Authentication guide](/configuration/authentication) to get one. ## Prerequisites ```bash theme={null} npm install axios ``` Set your API token as an environment variable: ```bash theme={null} export INBOX_API_TOKEN="your_token_here" ``` ## Step 1: Verify authentication Confirm your API token works by fetching your team information: ```typescript verify.ts theme={null} import axios from "axios"; const client = axios.create({ baseURL: "https://inboxapp.com/api/v1", headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); const { data: team } = await client.get("/team"); console.log("Connected to team:", team.name); ``` ```javascript verify.js theme={null} const axios = require("axios"); const client = axios.create({ baseURL: "https://inboxapp.com/api/v1", headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); const { data: team } = await client.get("/team"); console.log("Connected to team:", team.name); ``` ```bash cURL theme={null} curl https://inboxapp.com/api/v1/team \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} { "id": "hzcai5t59nn9vsck3rbuepyg", "name": "Acme Corp", "slug": "acme-corp", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": null, "currency": "USD", "allowSupportAccess": false } ``` ## Step 2: List your account links Account links are X accounts connected to your team. You need one to send messages: ```typescript theme={null} const { data: accountLinks } = await client.get("/account-links"); console.log(`Found ${accountLinks.length} account(s)`); const account = accountLinks[0]; console.log(`Using account: @${account.username}`); ``` **Response:** ```json theme={null} [ { "id": "df6jbw4h36qm5d9iu2sgn7kx", "platform": "twitter", "platformId": "1566123362161725440", "name": "Acme Corp", "username": "acmecorp", "image": "https://pbs.twimg.com/profile_images/...", "createdAt": "2024-06-01T12:00:00.000Z", "status": "active", "syncedAt": "2025-01-15T10:30:00.000Z" } ] ``` ## Step 3: Find or create a thread ### Option A: Lookup an existing thread If you know the prospect's X user ID: ```typescript theme={null} const { data: thread } = await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321", accountLinkId: account.id, }, }); if (thread) { console.log("Found existing thread:", thread.id); } else { console.log("No thread exists yet"); } ``` ### Option B: Create a new thread ```typescript theme={null} const { data: thread } = await client.post("/threads", { externalPlatformId: "1876543210987654321", accountLinkId: account.id, }); console.log("Created thread:", thread.id); ``` Creating a thread doesn't send a message. Use the [messages endpoint](/api-reference/messages/send-message) to send a DM. ## Step 4: Send a message Replying to contacts who have already messaged you works on all plans. Sending the **first message** to a new contact requires the Outbound Messages addon on a paid plan. See [Working with messages](/guides/messages#sending-messages) for details. ```typescript theme={null} const { data: message } = await client.post(`/threads/${thread.id}/messages`, { content: "Hello! Thanks for reaching out.", }); console.log("Message sent:", message.id); ``` **Response:** ```json theme={null} { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": "r3km7xj9wq5p2bvnhfdteoly", "campaignId": null, "content": "Hello! Thanks for reaching out.", "origin": "api", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": null, "isEdited": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null } ``` ## Complete example ```typescript send-message.ts theme={null} import axios from "axios"; const client = axios.create({ baseURL: "https://inboxapp.com/api/v1", headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); async function sendFirstMessage( prospectPlatformId: string, messageContent: string, ) { // 1. Verify authentication const { data: team } = await client.get("/team"); console.log("Connected to:", team.name); // 2. Get account link const { data: accountLinks } = await client.get("/account-links"); const account = accountLinks[0]; console.log(`Using account: @${account.username}`); // 3. Lookup or create thread let thread; const { data: existing } = await client.get("/threads/lookup", { params: { externalPlatformId: prospectPlatformId, accountLinkId: account.id, }, }); if (existing) { thread = existing; console.log("Found existing thread"); } else { const { data: created } = await client.post("/threads", { externalPlatformId: prospectPlatformId, accountLinkId: account.id, }); thread = created; console.log("Created new thread"); } // 4. Send message const { data: message } = await client.post( `/threads/${thread.id}/messages`, { content: messageContent }, ); console.log("Message sent:", message.id); return message; } sendFirstMessage("1876543210987654321", "Hello from the API!").catch((error) => console.error("Error:", error.response?.data ?? error.message), ); ``` ```javascript send-message.js theme={null} const axios = require("axios"); const client = axios.create({ baseURL: "https://inboxapp.com/api/v1", headers: { Authorization: `Bearer ${process.env.INBOX_API_TOKEN}`, "Content-Type": "application/json", }, }); async function sendFirstMessage(prospectPlatformId, messageContent) { // 1. Verify authentication const { data: team } = await client.get("/team"); console.log("Connected to:", team.name); // 2. Get account link const { data: accountLinks } = await client.get("/account-links"); const account = accountLinks[0]; console.log(`Using account: @${account.username}`); // 3. Lookup or create thread let thread; const { data: existing } = await client.get("/threads/lookup", { params: { externalPlatformId: prospectPlatformId, accountLinkId: account.id, }, }); if (existing) { thread = existing; console.log("Found existing thread"); } else { const { data: created } = await client.post("/threads", { externalPlatformId: prospectPlatformId, accountLinkId: account.id, }); thread = created; console.log("Created new thread"); } // 4. Send message const { data: message } = await client.post( `/threads/${thread.id}/messages`, { content: messageContent }, ); console.log("Message sent:", message.id); return message; } sendFirstMessage("1876543210987654321", "Hello from the API!").catch((error) => console.error("Error:", error.response?.data ?? error.message), ); ``` ```bash cURL theme={null} # 1. Get account links curl https://inboxapp.com/api/v1/account-links \ -H "Authorization: Bearer $INBOX_API_TOKEN" # 2. Lookup thread (replace PLATFORM_ID and ACCOUNT_LINK_ID) curl "https://inboxapp.com/api/v1/threads/lookup?externalPlatformId=1876543210987654321&accountLinkId=ACCOUNT_LINK_ID" \ -H "Authorization: Bearer $INBOX_API_TOKEN" # 3. Create thread if needed curl -X POST https://inboxapp.com/api/v1/threads \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"externalPlatformId": "1876543210987654321", "accountLinkId": "ACCOUNT_LINK_ID"}' # 4. Send message (replace THREAD_ID) curl -X POST "https://inboxapp.com/api/v1/threads/THREAD_ID/messages" \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"content": "Hello from the API!"}' ``` ## Quick send alternative For one-off messages, quick send combines thread lookup/creation and sending in one request: ```typescript quick-send.ts theme={null} const { data } = await client.post("/threads/messages", { externalPlatformId: "1876543210987654321", accountLinkId: account.id, content: "Hey, I saw your post and wanted to reach out!", }); console.log("Message sent:", data.message.id); console.log("Thread:", data.thread.id); ``` ```bash cURL theme={null} curl -X POST https://inboxapp.com/api/v1/threads/messages \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalPlatformId": "1876543210987654321", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "content": "Hey, I saw your post and wanted to reach out!" }' ``` **Response:** ```json theme={null} { "message": { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "threadId": "l44e15irdq4db30i77cgphhx", "content": "Hey, I saw your post and wanted to reach out!", "origin": "api", "createdAt": "2025-01-15T10:30:00.000Z" }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321" } } ``` Use quick send for simple scenarios. For more control over thread management, use the standard create/lookup + send workflow. ## Next steps Understand threads, prospects, and the data model Learn thread operations and filtering Manage prospect data and context Understand API limits # Error codes Source: https://docs.inboxapp.com/reference/error-codes Understanding and handling Inboxapp API errors ## Overview The Inboxapp API returns consistent error responses with actionable information. This guide covers all error codes, their causes, and recovery strategies. ## Error response format All errors follow this structure: ```json theme={null} { "error": { "code": "THREAD_NOT_FOUND", "message": "Thread with ID 'thread_invalid' not found", "status": 404 } } ``` | Field | Description | | --------- | ------------------------------------------ | | `code` | Programmatic identifier for the error type | | `message` | Human-readable description | | `status` | HTTP status code | ## HTTP status codes | Status | Meaning | Common Causes | | ------ | --------------------- | ------------------------------------ | | `400` | Bad Request | Invalid parameters, malformed JSON | | `401` | Unauthorized | Missing or invalid API token | | `403` | Forbidden | Token lacks required permissions | | `404` | Not Found | Resource doesn't exist | | `409` | Conflict | Duplicate resource or state conflict | | `422` | Unprocessable Entity | Valid JSON but invalid data | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Internal Server Error | Server-side issue | ## Authentication errors ### UNAUTHORIZED **Status:** 401 **When it occurs:** * API token is missing from the request * API token is invalid or malformed * API token has expired or been revoked **Example response:** ```json theme={null} { "error": { "code": "UNAUTHORIZED", "message": "Invalid or missing authentication token", "status": 401 } } ``` **How to fix:** 1. Verify the `Authorization` header is present: `Authorization: Bearer YOUR_TOKEN` 2. Check for extra spaces or newlines in the token 3. Generate a new token from your Inboxapp dashboard ```typescript auth-error.ts theme={null} try { const { data } = await client.get('/team'); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { console.error('Authentication failed. Check your API token.'); // Prompt user to re-authenticate or check token } } ``` ```bash cURL theme={null} # Verify your token format curl https://inboxapp.com/api/v1/team \ -H "Authorization: Bearer $INBOX_API_TOKEN" \ -v # Verbose output to see headers ``` ### FORBIDDEN **Status:** 403 **When it occurs:** * Token is valid but lacks access to the resource * Resource belongs to a different team * Feature not available on your plan **Example response:** ```json theme={null} { "error": { "code": "FORBIDDEN", "message": "Insufficient permissions to access this resource", "status": 403 } } ``` **How to fix:** 1. Verify the resource belongs to your team 2. Check if your plan includes the requested feature 3. Contact support if you believe this is an error *** ## Resource errors ### THREAD\_NOT\_FOUND **Status:** 404 **When it occurs:** * Thread ID doesn't exist * Thread was deleted * Using platform ID instead of Inbox ID **Example response:** ```json theme={null} { "error": { "code": "THREAD_NOT_FOUND", "message": "Thread with ID 'thread_invalid' not found", "status": 404 } } ``` **How to fix:** 1. Verify you're using the Inbox ID (CUID2 format like `l44e15irdq4db30i77cgphhx`), not the platform ID 2. Use `/threads/lookup` to find threads by prospect platform ID 3. Check if the thread was deleted ```typescript theme={null} // Wrong: Using platform ID await client.get(`/threads/1566123362161725440`); // Correct: Using Inbox ID await client.get(`/threads/l44e15irdq4db30i77cgphhx`); // Correct: Lookup by platform ID await client.get('/threads/lookup', { params: { externalPlatformId: '1876543210987654321', accountLinkId: 'df6jbw4h36qm5d9iu2sgn7kx' } }); ``` ### PROSPECT\_NOT\_FOUND **Status:** 404 **When it occurs:** * Prospect ID doesn't exist * Platform ID doesn't match any known prospect * Prospect was never in a conversation with your team **Example response:** ```json theme={null} { "error": { "code": "PROSPECT_NOT_FOUND", "message": "Prospect not found", "status": 404 } } ``` **How to fix:** 1. Verify the platform ID is correct (X user IDs are numeric strings) 2. Prospects are created when they first message you or when you start a thread 3. Use `/prospects/lookup` to check if a prospect exists ```typescript theme={null} // Check if prospect exists before operations const { data: prospect } = await client.get('/prospects/lookup', { params: { platformId: '1876543210987654321' } }); if (!prospect) { console.log('Prospect not in system. Start a conversation first.'); } ``` ### ACCOUNT\_LINK\_NOT\_FOUND **Status:** 404 **When it occurs:** * Account link ID doesn't exist * Using platform ID instead of Inbox ID * Account was disconnected **How to fix:** 1. List account links to get valid IDs: `GET /account-links` 2. Use the Inbox ID (CUID2 format), not the X user ID ```typescript theme={null} // Get valid account link IDs const { data: accountLinks } = await client.get('/account-links'); const validIds = accountLinks.map(a => a.id); console.log('Valid account link IDs:', validIds); ``` ### TAG\_NOT\_FOUND **Status:** 404 **When it occurs:** * Tag ID doesn't exist * Tag was deleted **How to fix:** 1. List tags to get valid IDs: `GET /tags` 2. Create the tag if it doesn't exist ### STATUS\_NOT\_FOUND **Status:** 404 **When it occurs:** * Status ID doesn't exist * Status was deleted **How to fix:** 1. List statuses to get valid IDs: `GET /statuses` 2. Create the status if it doesn't exist ### MEMBER\_NOT\_FOUND **Status:** 404 **When it occurs:** * Member ID doesn't exist * Member was removed from the team **How to fix:** 1. List members to get valid IDs: `GET /members` *** ## Validation errors ### VALIDATION\_ERROR **Status:** 400 **When it occurs:** * Required field is missing * Field value is wrong type * Value exceeds limits **Example response:** ```json theme={null} { "error": { "code": "VALIDATION_ERROR", "message": "Invalid request body", "status": 400, "details": { "field": "content", "issue": "Message content is required" } } } ``` **How to fix:** 1. Check required fields for the endpoint 2. Verify data types match the API spec 3. Ensure string lengths are within limits ```typescript theme={null} // Validate before sending function validateMessage(content: string) { if (!content?.trim()) { throw new Error('Message content is required'); } if (content.length > 10000) { throw new Error('Message exceeds 10,000 character limit'); } } ``` ### INVALID\_CURSOR **Status:** 400 **When it occurs:** * Cursor object is malformed * Cursor is from a different query * Cursor has expired **How to fix:** 1. Use cursors exactly as returned from the API 2. Don't modify cursor values 3. Start over from page 1 if cursor errors persist *** ## State errors ### DUPLICATE\_THREAD **Status:** 409 **When it occurs:** * Thread already exists for this prospect-account pair **How to fix:** Use lookup before creating: ```typescript theme={null} // Lookup first const { data: existing } = await client.get('/threads/lookup', { params: { externalPlatformId, accountLinkId } }); if (existing) { // Use existing thread return existing; } // Create only if none exists const { data: created } = await client.post('/threads', { externalPlatformId, accountLinkId }); ``` ### DUPLICATE\_TAG **Status:** 409 **When it occurs:** * Tag with same name already exists **How to fix:** Use a unique name or find the existing tag. ### DUPLICATE\_STATUS **Status:** 409 **When it occurs:** * Status with same name already exists **How to fix:** Use a unique name or find the existing status. *** ## Rate limiting ### RATE\_LIMITED **Status:** 429 **When it occurs:** * Too many requests in a short period **Example response:** ```json theme={null} { "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again in 60 seconds.", "status": 429 } } ``` **Response headers:** ``` Retry-After: 60 X-RateLimit-Limit: 300 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1705312800 ``` **How to fix:** 1. Check the `Retry-After` header for wait time 2. Implement exponential backoff 3. Reduce request frequency See the [Rate Limits guide](/reference/rate-limits) for detailed limits and backoff strategies. *** ## Error handling pattern A comprehensive error handler for all error types: ```typescript theme={null} import axios, { AxiosError } from 'axios'; interface InboxError { code: string; message: string; status: number; details?: Record; } async function handleInboxError(error: AxiosError<{ error: InboxError }>) { if (!axios.isAxiosError(error) || !error.response) { throw error; // Not an API error } const { status, data, headers } = error.response; const errorInfo = data?.error; switch (status) { case 401: console.error('Authentication failed. Check your API token.'); // Redirect to re-authenticate or refresh token break; case 403: console.error('Permission denied:', errorInfo?.message); break; case 404: console.error('Resource not found:', errorInfo?.code); // Handle based on error code if (errorInfo?.code === 'THREAD_NOT_FOUND') { // Maybe create the thread instead } break; case 409: console.error('Conflict:', errorInfo?.message); // Handle duplicates - usually fetch existing resource break; case 429: const retryAfter = parseInt(headers['retry-after'] || '60', 10); console.log(`Rate limited. Retry in ${retryAfter} seconds.`); await new Promise(r => setTimeout(r, retryAfter * 1000)); // Retry the request break; case 500: console.error('Server error. Please try again later.'); break; default: console.error('API error:', errorInfo?.message || error.message); } throw error; } // Usage try { const { data } = await client.get(`/threads/${threadId}`); } catch (error) { await handleInboxError(error as AxiosError); } ``` ## Best practices Error messages may change, but codes are stable: ```typescript theme={null} // Good: Check code if (error.response?.data?.error?.code === 'THREAD_NOT_FOUND') { // Handle missing thread } // Bad: Check message string if (error.message.includes('not found')) { // Fragile } ``` Catch validation errors locally before making API calls to improve user experience. Include request details for debugging: ```typescript theme={null} console.error('API Error:', { endpoint: '/threads', params: { threadId }, error: error.response?.data }); ``` Missing resources are often recoverable - create them or suggest alternatives. ## Related Rate limiting details and backoff API token setup Common issues and fixes Complete endpoint documentation # Pagination Source: https://docs.inboxapp.com/reference/pagination Handling paginated responses from the Inboxapp API ## Overview The Inboxapp API uses cursor-based pagination for list endpoints that can return large datasets. This approach provides consistent results even when data changes between requests. Only `GET /threads` and `GET /threads/{threadId}/messages` are paginated. Other list endpoints (`GET /account-links`, `GET /members`, `GET /tags`, `GET /statuses`) return all results as a flat array with no pagination. ## Response structure Paginated endpoints return items and an optional `nextCursor`: ```json theme={null} { "threads": [ { "id": "l44e15irdq4db30i77cgphhx", "..." }, { "id": "m55f26jsep5ec41j88dhqiiy", "..." } ], "nextCursor": { "id": "m55f26jsep5ec41j88dhqiiy", "timestamp": "2025-01-15T10:30:00.000Z" } } ``` | Field | Description | | ----------------------- | ------------------------------------------------------------------ | | `threads` or `messages` | Array of results (key varies by endpoint) | | `nextCursor` | Object containing `id` and `timestamp`. `null` when no more pages. | ## Basic usage Pass `cursorId` and `cursorTimestamp` from the previous response's `nextCursor` to fetch the next page: ```typescript pagination.ts theme={null} import axios from 'axios'; const client = axios.create({ baseURL: 'https://inboxapp.com/api/v1', headers: { 'Authorization': `Bearer ${process.env.INBOX_API_TOKEN}`, 'Content-Type': 'application/json' } }); async function getAllThreads() { const allThreads: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get('/threads', { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return allThreads; } const threads = await getAllThreads(); console.log(`Fetched ${threads.length} threads`); ``` ```javascript pagination.js theme={null} const axios = require('axios'); const client = axios.create({ baseURL: 'https://inboxapp.com/api/v1', headers: { 'Authorization': `Bearer ${process.env.INBOX_API_TOKEN}`, 'Content-Type': 'application/json' } }); async function getAllThreads() { const allThreads = []; let cursorId; let cursorTimestamp; do { const params = { limit: 100 }; if (cursorId) { params.cursorId = cursorId; params.cursorTimestamp = cursorTimestamp; } const { data } = await client.get('/threads', { params }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return allThreads; } getAllThreads().then(threads => { console.log(`Fetched ${threads.length} threads`); }); ``` ```bash cURL theme={null} # First request curl "https://inboxapp.com/api/v1/threads?limit=100" \ -H "Authorization: Bearer $INBOX_API_TOKEN" # Subsequent request with cursor curl "https://inboxapp.com/api/v1/threads?limit=100&cursorId=m55f26jsep5ec41j88dhqiiy&cursorTimestamp=2025-01-15T10:30:00.000Z" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` Use the flat `cursorId` and `cursorTimestamp` parameters. The bracket notation form (`cursor[id]`, `cursor[timestamp]`) is also supported but deprecated. ## Page size | Endpoint | Default | Maximum | | ---------------------------- | ------- | ------- | | `GET /threads` | 20 | 100 | | `GET /threads/{id}/messages` | 20 | 100 | Request more items per page to reduce API calls: ```typescript theme={null} const { data } = await client.get('/threads', { params: { limit: 100 } }); ``` ## Streaming pattern For real-time processing without loading everything into memory: ```typescript theme={null} async function* streamThreads() { let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get('/threads', { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); for (const thread of data.threads) { yield thread; } cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); } for await (const thread of streamThreads()) { await processThread(thread); } ``` ## Pagination with filters Filters apply before pagination. The cursor tracks position within filtered results: ```typescript theme={null} async function getFilteredThreads(tagId: string) { const allThreads: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get('/threads', { params: { 'filters[tags][selectedIds][0]': tagId, limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return allThreads; } ``` ## Error handling Handle rate limits and transient errors during pagination: ```typescript theme={null} async function getAllThreadsWithRetry() { const allThreads: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; let retries = 0; const maxRetries = 3; while (true) { try { const { data } = await client.get('/threads', { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; retries = 0; if (!cursorId) break; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 429) { const retryAfter = error.response.headers['retry-after'] || 60; await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } if (retries < maxRetries) { retries++; await new Promise(r => setTimeout(r, 1000 * retries)); continue; } throw error; } } return allThreads; } ``` ## Best practices Use the generator pattern to process items as they arrive instead of loading everything into memory. Always include rate limit handling in pagination code. Bulk fetches can hit the 300 requests/minute limit quickly. Treat cursor values as opaque. Don't modify or construct them manually — always use values returned from the API. ## Related Encoding rules for complex parameters Understanding API rate limits Threads endpoint reference Messages endpoint reference # Query parameters Source: https://docs.inboxapp.com/reference/query-parameters Encoding objects and arrays in Inboxapp API query parameters ## Overview The Inboxapp API uses bracket notation for encoding complex data types (objects and arrays) in query parameters. This guide covers the encoding rules and common patterns. ## Objects Objects are encoded using bracket notation with property names. **Object value:** ```json theme={null} { "id": "l44e15irdq4db30i77cgphhx", "timestamp": "2025-01-15T10:30:00.000Z" } ``` **Encoded in URL:** ``` ?cursorId=l44e15irdq4db30i77cgphhx&cursorTimestamp=2025-01-15T10:30:00.000Z ``` ### Cursor example ```typescript cursor.ts theme={null} // Preferred: flat cursor parameters const { data } = await client.get('/threads', { params: { cursorId: 'l44e15irdq4db30i77cgphhx', cursorTimestamp: '2025-01-15T10:30:00.000Z', limit: 50 } }); ``` ```javascript cursor.js theme={null} // Preferred: flat cursor parameters const params = new URLSearchParams(); params.append('cursorId', 'l44e15irdq4db30i77cgphhx'); params.append('cursorTimestamp', '2025-01-15T10:30:00.000Z'); params.append('limit', '50'); const response = await fetch( `https://inboxapp.com/api/v1/threads?${params.toString()}`, { headers: { 'Authorization': `Bearer ${token}` } } ); ``` ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/threads?cursorId=l44e15irdq4db30i77cgphhx&cursorTimestamp=2025-01-15T10:30:00.000Z&limit=50" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` Use the flat `cursorId` and `cursorTimestamp` parameters for pagination. The bracket notation form (`cursor[id]`, `cursor[timestamp]`) is also supported but deprecated. ## Arrays Arrays can be encoded in three ways. All are supported by the API. **Array value:** ```json theme={null} ["df6jbw4h36qm5d9iu2sgn7kx", "g7ht2kw9xr5m8fp3yjnbqvce"] ``` ### Option 1: repeated parameters (simplest) ``` ?accountLinkIds=df6jbw4h36qm5d9iu2sgn7kx&accountLinkIds=g7ht2kw9xr5m8fp3yjnbqvce ``` ### Option 2: empty bracket notation Explicitly denotes an array. Useful when there's only one value: ``` ?accountLinkIds[]=df6jbw4h36qm5d9iu2sgn7kx&accountLinkIds[]=g7ht2kw9xr5m8fp3yjnbqvce ``` ### Option 3: indexed bracket notation Most explicit. Allows reordering and sparse arrays: ``` ?accountLinkIds[0]=df6jbw4h36qm5d9iu2sgn7kx&accountLinkIds[1]=g7ht2kw9xr5m8fp3yjnbqvce ``` ## Thread filters The `GET /threads` endpoint supports advanced filtering via the `filters` parameter using nested bracket notation. ### By tags ```typescript theme={null} // Threads with ANY of these tags (OR logic) const { data } = await client.get('/threads', { params: { 'filters[tags][selectedIds][0]': 't8rvk2m5j0xn4wq7ybftcael', 'filters[tags][selectedIds][1]': 'u9swl3n6k1yo5xr8zcgudbfm' } }); // Threads with NO tags const { data: untagged } = await client.get('/threads', { params: { 'filters[tags][noTags]': true } }); ``` ### By status ```typescript theme={null} // Threads with prospects in specific statuses const { data } = await client.get('/threads', { params: { 'filters[statuses][selectedIds][0]': 's3km7xj9wq5p2bvnhfdteoly', 'filters[statuses][selectedIds][1]': 's4ln8yk0xr6q3cw1behsifup' } }); // Threads with no status set const { data: noStatus } = await client.get('/threads', { params: { 'filters[statuses][noStatus]': true } }); ``` ### By assignee ```typescript theme={null} // Threads assigned to specific members const { data } = await client.get('/threads', { params: { 'filters[assignees][selectedIds][0]': 'r3km7xj9wq5p2bvnhfdteoly' } }); // Unassigned threads only const { data: unassigned } = await client.get('/threads', { params: { 'filters[assignees][noAssignee]': true } }); ``` ### By campaign ```typescript theme={null} // Threads from specific campaigns const { data } = await client.get('/threads', { params: { 'filters[campaigns][selectedIds][0]': 'c5nwp8r2j3ym6at0xbkqhfvg' } }); // Threads not from any campaign const { data: noCampaign } = await client.get('/threads', { params: { 'filters[campaigns][noCampaign]': true } }); ``` ### Sort order ```typescript theme={null} const { data } = await client.get('/threads', { params: { 'filters[sortOrder]': 'desc' // or 'asc' } }); ``` ## Combined filters Multiple filters combine with AND logic: ```typescript theme={null} // Active threads + qualified status + assigned to specific member const { data } = await client.get('/threads', { params: { inbox: 'default', 'accountLinkIds[0]': 'df6jbw4h36qm5d9iu2sgn7kx', 'filters[statuses][selectedIds][0]': 's3km7xj9wq5p2bvnhfdteoly', 'filters[assignees][selectedIds][0]': 'r3km7xj9wq5p2bvnhfdteoly', limit: 50 } }); ``` ## Filter parameter reference | Filter | Parameter | Description | | ------------------- | ------------------------------------ | ---------------------------------------------------------- | | Tags (include) | `filters[tags][selectedIds][N]` | Show threads with any of these tag IDs | | Tags (exclude) | `filters[tags][noTags]` | Show only untagged threads | | Statuses (include) | `filters[statuses][selectedIds][N]` | Show threads with any of these status IDs | | Statuses (exclude) | `filters[statuses][noStatus]` | Show only threads with no status | | Assignees (include) | `filters[assignees][selectedIds][N]` | Show threads assigned to these member IDs | | Assignees (exclude) | `filters[assignees][noAssignee]` | Show only unassigned threads | | Campaigns (include) | `filters[campaigns][selectedIds][N]` | Show threads from these campaigns | | Campaigns (exclude) | `filters[campaigns][noCampaign]` | Show only non-campaign threads | | Sort order | `filters[sortOrder]` | `"desc"` (newest first, default) or `"asc"` (oldest first) | | Inbox view | `inbox` | `"default"`, `"no-reply"`, `"requests"`, or `"archived"` | | Account links | `accountLinkIds[N]` | Filter by specific account link IDs | | Page size | `limit` | `1`–`100` (default `20`) | ## URL encoding special characters Values containing special characters must be URL-encoded: | Character | Encoded | | --------- | ------------ | | Space | `%20` or `+` | | `+` | `%2B` | | `&` | `%26` | | `=` | `%3D` | | `[` | `%5B` | | `]` | `%5D` | | `:` | `%3A` | Most HTTP clients handle this automatically. ## Helper function A utility for encoding filter parameters: ```typescript theme={null} function buildFilterParams(filters: { tagIds?: string[]; statusIds?: string[]; assigneeIds?: string[]; campaignIds?: string[]; noTags?: boolean; noStatus?: boolean; noAssignee?: boolean; noCampaign?: boolean; sortOrder?: 'asc' | 'desc'; }): Record { const params: Record = {}; filters.tagIds?.forEach((id, i) => { params[`filters[tags][selectedIds][${i}]`] = id; }); filters.statusIds?.forEach((id, i) => { params[`filters[statuses][selectedIds][${i}]`] = id; }); filters.assigneeIds?.forEach((id, i) => { params[`filters[assignees][selectedIds][${i}]`] = id; }); filters.campaignIds?.forEach((id, i) => { params[`filters[campaigns][selectedIds][${i}]`] = id; }); if (filters.noTags) params['filters[tags][noTags]'] = true; if (filters.noStatus) params['filters[statuses][noStatus]'] = true; if (filters.noAssignee) params['filters[assignees][noAssignee]'] = true; if (filters.noCampaign) params['filters[campaigns][noCampaign]'] = true; if (filters.sortOrder) params['filters[sortOrder]'] = filters.sortOrder; return params; } // Usage const { data } = await client.get('/threads', { params: { inbox: 'default', ...buildFilterParams({ tagIds: ['t8rvk2m5j0xn4wq7ybftcael'], noAssignee: true }), limit: 50 } }); ``` ## Common mistakes **Wrong:** Using incorrect filter key structure ```typescript theme={null} // These parameter names are WRONG const { data } = await client.get('/threads', { params: { 'filter[tagIds][0]': 'tag_abc', // Wrong key 'filter[assignedToMemberIds][0]': 'member_abc', // Wrong key 'filter[noAssignee]': true, // Wrong key inboxView: 'default', // Wrong param name accountLinkId: 'acc_abc', // Wrong param name } }); ``` **Correct:** ```typescript theme={null} const { data } = await client.get('/threads', { params: { 'filters[tags][selectedIds][0]': 't8rvk2m5j0xn4wq7ybftcael', 'filters[assignees][selectedIds][0]': 'r3km7xj9wq5p2bvnhfdteoly', 'filters[assignees][noAssignee]': true, inbox: 'default', 'accountLinkIds[0]': 'df6jbw4h36qm5d9iu2sgn7kx', } }); ``` ## Related Cursor-based pagination patterns Thread filtering examples Threads endpoint reference API overview and encoding rules # Rate limits Source: https://docs.inboxapp.com/reference/rate-limits Understanding and handling Inboxapp API rate limits ## Overview The Inboxapp API enforces rate limits to ensure fair usage and platform stability. This guide covers the limits, how to detect them, and strategies for handling them gracefully. ## Current limits Rate limits are **global per team** — all endpoints share the same limits, and all API tokens for a team share the same quota. | Window | Limit | | ---------- | ---------------- | | Per minute | 300 requests | | Per hour | 10,000 requests | | Per day | 100,000 requests | All three windows are enforced simultaneously using a sliding window. If any window is exceeded, requests are rejected until that window resets. Limits are per team, not per token. Multiple API tokens for the same team share the same rate limit quota. ## Rate limit headers Every response includes rate limit information: ``` X-RateLimit-Limit: 300 X-RateLimit-Remaining: 295 X-RateLimit-Reset: 1705312800 ``` | Header | Description | | ----------------------- | -------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed in the window | | `X-RateLimit-Remaining` | Requests remaining in current window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | ## Rate limit responses When you exceed the limit: **Status:** 429 Too Many Requests ```json theme={null} { "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again in 60 seconds.", "status": 429 } } ``` **Headers:** ``` Retry-After: 60 ``` ## Exponential backoff The recommended pattern for handling rate limits: ```typescript backoff.ts theme={null} import axios, { AxiosError } from 'axios'; async function withRetry( fn: () => Promise, options: { maxRetries?: number; baseDelay?: number; maxDelay?: number; } = {} ): Promise { const { maxRetries = 5, baseDelay = 1000, maxDelay = 60000 } = options; let attempt = 0; while (true) { try { return await fn(); } catch (error) { if (!axios.isAxiosError(error)) throw error; const status = error.response?.status; // Only retry on rate limits and server errors if (status !== 429 && (status < 500 || status >= 600)) { throw error; } attempt++; if (attempt >= maxRetries) { throw new Error(`Max retries (${maxRetries}) exceeded`); } // Calculate delay let delay: number; if (status === 429) { // Use Retry-After header if available const retryAfter = error.response?.headers['retry-after']; delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : baseDelay * Math.pow(2, attempt); } else { // Exponential backoff for server errors delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay); } console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Usage const thread = await withRetry(() => client.get(`/threads/${threadId}`)); ``` ```javascript backoff.js theme={null} async function withRetry(fn, options = {}) { const { maxRetries = 5, baseDelay = 1000, maxDelay = 60000 } = options; let attempt = 0; while (true) { try { return await fn(); } catch (error) { const status = error.response?.status; if (status !== 429 && (status < 500 || status >= 600)) { throw error; } attempt++; if (attempt >= maxRetries) { throw new Error(`Max retries (${maxRetries}) exceeded`); } let delay; if (status === 429) { const retryAfter = error.response?.headers['retry-after']; delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : baseDelay * Math.pow(2, attempt); } else { delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay); } console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Usage withRetry(() => client.get(`/threads/${threadId}`)) .then(response => console.log(response.data)); ``` ## Bulk operations For operations that process many items, implement rate-aware batching: ```typescript theme={null} async function bulkSendMessages( threads: Array<{ id: string; message: string }> ) { const results = []; for (const thread of threads) { try { const { data } = await withRetry(() => client.post(`/threads/${thread.id}/messages`, { content: thread.message }) ); results.push({ threadId: thread.id, success: true, messageId: data.id }); } catch (error) { results.push({ threadId: thread.id, success: false, error: error.message }); } // Small delay between sends to stay well within 300/min await new Promise(resolve => setTimeout(resolve, 500)); } return results; } ``` ## Proactive rate limit tracking Track your usage to avoid hitting limits: ```typescript theme={null} class RateLimitTracker { private remaining: number; private resetTime: number; constructor() { this.remaining = 300; this.resetTime = Date.now() + 60000; } update(headers: Record) { this.remaining = parseInt(headers['x-ratelimit-remaining'] || '300', 10); this.resetTime = parseInt(headers['x-ratelimit-reset'] || '0', 10) * 1000; } async waitIfNeeded() { if (this.remaining <= 5) { const waitTime = Math.max(0, this.resetTime - Date.now()); if (waitTime > 0) { console.log(`Approaching rate limit. Waiting ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } } canMakeRequest(): boolean { return this.remaining > 0 || Date.now() > this.resetTime; } } // Usage with axios interceptor const tracker = new RateLimitTracker(); client.interceptors.response.use( response => { tracker.update(response.headers); return response; }, error => { if (error.response?.headers) { tracker.update(error.response.headers); } return Promise.reject(error); } ); // Before each request async function makeRequest(fn: () => Promise) { await tracker.waitIfNeeded(); return fn(); } ``` ## Pagination with rate limits When paginating through large datasets: ```typescript theme={null} async function getAllThreadsWithRateLimits() { const allThreads = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; let requestCount = 0; do { const { data, headers } = await client.get('/threads', { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); allThreads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; requestCount++; // Check remaining requests const remaining = parseInt(headers['x-ratelimit-remaining'] || '300', 10); if (remaining < 10 && cursorId) { const resetTime = parseInt(headers['x-ratelimit-reset'] || '0', 10) * 1000; const waitTime = Math.max(0, resetTime - Date.now()); console.log(`Rate limit approaching. Waiting ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } while (cursorId); return allThreads; } ``` ## Best practices Cache read results to avoid repeated requests: ```typescript theme={null} const cache = new Map(); async function getCachedThread(threadId: string) { const cached = cache.get(threadId); if (cached && cached.expiry > Date.now()) { return cached.data; } const { data: thread } = await client.get(`/threads/${threadId}`); cache.set(threadId, { data: thread, expiry: Date.now() + 60000 // 1 minute cache }); return thread; } ``` Track `X-RateLimit-Remaining` and pause before hitting zero. Stop making requests temporarily when consistently hitting limits. ## Platform considerations Sending too many messages too quickly may trigger platform-level restrictions on your X account, separate from API rate limits. Space out messages appropriately, especially for outreach. ## Related All error types including rate limits Efficient data fetching Send and receive messages Common issues and fixes # FAQ Source: https://docs.inboxapp.com/resources/faq Frequently asked questions about the Inboxapp API ## General Inboxapp is a DM management platform with a developer API and a team inbox interface, starting with X (Twitter). The API gives you programmatic access to conversations, prospects, and team workflows. Common use cases include: * **Messaging automation** — Send DMs programmatically with custom workflows * **CRM integration** — Sync conversations to HubSpot, Salesforce, etc. * **AI chatbots** — Build intelligent auto-reply systems * **Analytics dashboards** — Track engagement and conversion metrics Currently, X (Twitter) DMs are supported. Instagram and LinkedIn are coming in Q2 2026, with TikTok and WhatsApp planned after that. See [Supported platforms](/resources/supported-platforms) for the full roadmap. All plans support a 7-day free trial. API access is available by default. Replying to contacts who have already messaged you works on all plans at no extra cost. Sending the first message to a new contact requires the **Outbound Messages addon** (\$199/mo) on a paid plan. Enable it in **Settings → Billing** or email [support@inboxapp.com](mailto:support@inboxapp.com). *** ## IDs and lookups Every entity has two IDs: | ID Type | Field | Example | Use For | | --------------- | ------------ | -------------------------- | -------------------------- | | **Inbox ID** | `id` | `l44e15irdq4db30i77cgphhx` | All API operations | | **Platform ID** | `platformId` | `1876543210987654321` | Lookups from external data | Always use Inbox IDs for API calls. Use platform IDs for lookups. ```typescript theme={null} // Correct: Inbox ID for operations await client.get(`/threads/${thread.id}`); // Correct: Platform ID for lookups await client.get("/threads/lookup", { params: { externalPlatformId: "1876543210987654321" }, }); ``` Use the lookup-by-username endpoint: ```typescript theme={null} const { data: threads } = await client.get("/threads/lookup-by-username", { params: { username: "jordanrivera" }, }); ``` Returns a flat array of threads across all account links for that username. The username must be an exact match (case-insensitive the `@` prefix). For lookup by platform ID instead, use `GET /threads/lookup` with `externalPlatformId` and `accountLinkId`. The Inboxapp API doesn't provide this. Use the X API to lookup users by username: ```bash theme={null} curl "https://api.twitter.com/2/users/by/username/johndoe" \ -H "Authorization: Bearer YOUR_X_BEARER_TOKEN" ``` *** ## Messaging Yes, using the quick send endpoint: ```typescript theme={null} await client.post("/threads/messages", { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", content: "Hello!", }); ``` This creates the thread and prospect automatically. Rate limits are **global per team** — all endpoints share the same limits, and all API tokens for a team share the same quota. | Window | Limit | | ---------- | ---------------- | | Per minute | 300 requests | | Per hour | 10,000 requests | | Per day | 100,000 requests | See the [Rate Limits guide](/reference/rate-limits) for details and backoff strategies. X DMs support up to 10,000 characters. Media attachments are not currently supported via the API. Only text messages can be sent. | Method | Use When | | ------------------------------------------------- | ------------------------------------------------- | | **Quick send** (`POST /threads/messages`) | Simple one-off messages, don't need thread object | | **Standard send** (`POST /threads/{id}/messages`) | Multiple messages, need thread details first | Quick send handles thread creation automatically. *** ## Threads and conversations | View | Description | | ---------- | --------------------------------------------------- | | `default` | Active conversations needing attention | | `no-reply` | Prospect sent the last message, awaiting your reply | | `requests` | New message requests | | `archived` | Marked as done | ```typescript theme={null} const { data } = await client.get("/threads", { params: { inbox: "default" }, }); ``` Set `done: true`: ```typescript theme={null} await client.patch(`/threads/${threadId}`, { done: true, }); ``` Yes, but it's permanent: ```typescript theme={null} await client.delete(`/threads/${threadId}`); ``` Consider archiving (`done: true`) instead. Check `authorId` against your account link IDs. There is no `direction` field on messages. ```typescript theme={null} const { data: accountLinks } = await client.get("/account-links"); const accountLinkIds = new Set(accountLinks.map((a) => a.id)); const { data } = await client.get(`/threads/${threadId}/messages`, { params: { limit: 1 }, }); const lastMessage = data.messages[0]; if (lastMessage && accountLinkIds.has(lastMessage.authorId)) { console.log("Last message was from our account"); } else { console.log("Last message was from the prospect"); } ``` *** ## Prospects Prospects are automatically created when: 1. They send you a DM 2. You create a thread with them 3. You use quick send to message them You cannot create prospects directly. Use the context update endpoint: ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { notes: "Interested in Enterprise plan", valuation: 50000, }); ``` No. Profile data (followers, bio, etc.) is synced periodically. Check `isFresh`, `isStale`, and `confidence` to assess data quality. Yes. Tags are multi-select. Statuses are single-select. Tags are updated incrementally: ```typescript theme={null} await client.patch(`/prospects/${prospectId}/context`, { addTags: ["t8rvk2m5j0xn4wq7ybftcael", "u9swl3n6k1yo5xr8zcgudbfm"], statusId: "s3km7xj9wq5p2bvnhfdteoly", }); ``` There is no `tagIds` field — use `addTags` and `removeTags` to modify tags. *** ## Account links Account connections are managed through the Inboxapp dashboard: 1. Go to [inboxapp.com](https://inboxapp.com) 2. Navigate to **Settings → Accounts** 3. Make sure you have the [Chrome extension installed](https://chromewebstore.google.com/detail/inbox/manipndiogdfeenejomojogbbcbenioe) 4. Click **Open X** and select the account to link The API is read-only for account links. Yes. Specify the `accountLinkId` when sending: ```typescript theme={null} await client.post("/threads/messages", { externalPlatformId: "1876543210987654321", accountLinkId: "df6jbw4h36qm5d9iu2sgn7kx", content: "Hello from sales!", }); ``` Rate limits are per team — all API tokens for the same team share the same quota. The limits are 300 requests per minute, 10,000 per hour, and 100,000 per day across all endpoints. *** ## Team and members ```typescript theme={null} await client.patch(`/threads/${threadId}`, { assigneeId: 'r3km7xj9wq5p2bvnhfdteoly' }); ``` No. Team member management is done through the Inboxapp dashboard. ```typescript theme={null} const { data } = await client.get('/threads', { params: { 'filters[assignees][noAssignee]': true } }); ``` *** ## Technical `https://inboxapp.com/api/v1` Use cursor-based pagination with `cursorId` and `cursorTimestamp`. Only `GET /threads` and `GET /threads/{id}/messages` are paginated — other list endpoints return all results as a flat array. ```typescript theme={null} let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get("/threads", { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }), }, }); // Process data.threads cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); ``` See the [Pagination guide](/reference/pagination). Use the `filters` parameter with bracket notation: ```typescript theme={null} const { data } = await client.get("/threads", { params: { inbox: "default", "filters[tags][selectedIds][0]": "t8rvk2m5j0xn4wq7ybftcael", "filters[assignees][noAssignee]": true, }, }); ``` See the [Query Parameters guide](/reference/query-parameters) for a full reference of filter keys. Webhooks are not currently available. Use polling to check for new messages. Not yet. Use any HTTP client (axios, fetch) with the REST API. *** ## Still have questions? * Browse the [API Reference](/api-reference/introduction) for endpoint details * Check [Troubleshooting](/resources/troubleshooting) for common issues * Visit the [Help Center](/help) * Start a chat with the button in the bottom-right corner * Email support at [support@inboxapp.com](mailto:support@inboxapp.com) Complete endpoint documentation Common issues and fixes Send your first message Understand the data model # Supported platforms Source: https://docs.inboxapp.com/resources/supported-platforms Current platform availability and upcoming support for Instagram, LinkedIn, and more ## Current support The Inboxapp API currently supports **X (Twitter) DMs**. All API endpoints, webhooks, and features documented here work with X. ## Platform roadmap | Platform | Status | Expected | | --------------- | ----------- | -------- | | **X (Twitter)** | Available | — | | **Instagram** | Coming soon | Q2 2026 | | **LinkedIn** | Coming soon | Q2 2026 | | **TikTok** | Planned | TBD | | **WhatsApp** | Planned | TBD | The API is designed to be platform-agnostic. When new platforms launch, your existing integration patterns (threads, messages, prospects) will work the same way — the `platform` field on objects will reflect the new platform. ## What "platform-agnostic" means for your code The data model uses generic concepts that map across platforms: | Inboxapp concept | X (Twitter) | Instagram | LinkedIn | | ---------------- | ------------------- | -------------------- | -------------------- | | Thread | DM conversation | DM conversation | Message thread | | Message | Direct message | Direct message | InMail / message | | Prospect | X user | Instagram user | LinkedIn user | | Account link | Connected X account | Connected IG account | Connected LI account | Code you write today against the X integration will require minimal changes when new platforms are available. The main difference will be the `platform` field value on API objects. ## Need a specific platform? If your use case depends on a platform not yet supported, reach out. Platform prioritization is driven by customer demand. Email **[support@inboxapp.com](mailto:support@inboxapp.com)** with your platform needs, use case, and timeline. This directly influences our roadmap. # Troubleshooting Source: https://docs.inboxapp.com/resources/troubleshooting Common issues and solutions for the Inboxapp API ## Authentication issues ### 401 Unauthorized **Symptom:** All requests return 401 status. **Common causes:** 1. Missing `Authorization` header 2. Typo in token (extra spaces, newlines) 3. Expired or revoked token 4. Missing `Bearer ` prefix **Solutions:** ```typescript theme={null} // Check your header format const headers = { 'Authorization': `Bearer ${process.env.INBOX_API_TOKEN}`, // Note the space after Bearer 'Content-Type': 'application/json' }; // Verify token is loaded if (!process.env.INBOX_API_TOKEN) { throw new Error('INBOX_API_TOKEN environment variable not set'); } // Test with a simple request const response = await fetch('https://inboxapp.com/api/v1/team', { headers }); console.log('Status:', response.status); ``` **Verification steps:** 1. Print your token (first/last 4 chars only) to verify it's loaded 2. Try the token in cURL to isolate the issue 3. Generate a new token from the dashboard *** ### 403 Forbidden **Symptom:** Token works for some endpoints but not others. **Common causes:** 1. Accessing a resource from another team 2. Feature not available on your plan 3. Attempting an admin-only action **Solutions:** 1. Verify the resource belongs to your team 2. Check your plan in the dashboard 3. Use a token from an admin account *** ## Resource not found (404) ### Thread not found **Symptom:** `THREAD_NOT_FOUND` error when accessing a thread. **Common causes:** 1. Using platform ID instead of Inbox ID 2. Thread was deleted 3. Typo in thread ID **Solution:** ```typescript theme={null} // Wrong: Using platform ID const threadPlatformId = '1566123362161725440:1876543210987654321'; await client.get(`/threads/${threadPlatformId}`); // 404! // Correct: Use Inbox ID (CUID2 format) const threadInboxId = 'l44e15irdq4db30i77cgphhx'; await client.get(`/threads/${threadInboxId}`); // Works // Or use lookup with platform ID const { data: thread } = await client.get('/threads/lookup', { params: { externalPlatformId: '1876543210987654321', accountLinkId: 'df6jbw4h36qm5d9iu2sgn7kx' } }); ``` *** ### Prospect not found **Symptom:** `PROSPECT_NOT_FOUND` when sending a message or looking up. **Common causes:** 1. Prospect has never interacted with your team 2. Wrong platform ID format 3. Prospect was deleted **Solutions:** ```typescript theme={null} // Verify prospect exists before operations const { data: prospect } = await client.get('/prospects/lookup', { params: { platformId: '1876543210987654321' } }); if (!prospect) { console.log('Prospect not in system. They need to message you first,'); console.log('or you need to start a conversation via /threads endpoint.'); } ``` Prospects are auto-created when they message you or when you create a thread with them. You cannot create prospects directly. *** ### Account link not found **Symptom:** `ACCOUNT_LINK_NOT_FOUND` when sending messages. **Common causes:** 1. Using platform ID instead of Inbox ID 2. Account was disconnected 3. Typo in account link ID **Solution:** ```typescript theme={null} // List your account links to get valid IDs const { data: accountLinks } = await client.get('/account-links'); console.log('Valid account link IDs:'); accountLinks.forEach((a: any) => { console.log(` @${a.username}: ${a.id}`); }); ``` *** ## Rate limiting ### 429 Too many requests **Symptom:** Requests start failing with 429 after sending many messages. **Solutions:** ```typescript theme={null} // Implement retry with backoff async function sendWithRetry(threadId: string, text: string) { let retries = 0; const maxRetries = 5; while (retries < maxRetries) { try { return await client.post(`/threads/${threadId}/messages`, { text }); } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 429) { const retryAfter = parseInt( error.response.headers['retry-after'] || '60', 10 ); console.log(`Rate limited. Waiting ${retryAfter}s...`); await new Promise(r => setTimeout(r, retryAfter * 1000)); retries++; } else { throw error; } } } throw new Error('Max retries exceeded'); } // Add delays between bulk operations for (const message of messages) { await sendMessage(message); await new Promise(r => setTimeout(r, 6000)); // 6 seconds between sends } ``` See the [Rate Limits guide](/reference/rate-limits) for detailed limits. *** ## Pagination issues ### Missing data **Symptom:** Not all threads/messages are returned. **Cause:** Not handling pagination properly. **Solution:** ```typescript theme={null} // Always paginate through all results async function getAllThreads() { const threads: any[] = []; let cursorId: string | undefined; let cursorTimestamp: string | undefined; do { const { data } = await client.get('/threads', { params: { limit: 100, ...(cursorId && { cursorId, cursorTimestamp }) } }); threads.push(...data.threads); cursorId = data.nextCursor?.id; cursorTimestamp = data.nextCursor?.timestamp; } while (cursorId); return threads; } ``` *** ### Invalid cursor **Symptom:** `INVALID_CURSOR` error when paginating. **Common causes:** 1. Cursor was modified 2. Cursor from a different query 3. Cursor format is wrong **Solutions:** 1. Use cursors exactly as returned (don't modify) 2. Start fresh if cursor errors persist 3. Check cursor parameter names ```typescript theme={null} // Correct: use flat cursor parameters const { data } = await client.get('/threads', { params: { cursorId: nextCursor.id, cursorTimestamp: nextCursor.timestamp } }); ``` *** ## Message sending issues ### Message not delivered **Symptom:** API returns success but message doesn't appear on X. **Possible causes:** 1. Platform-level restrictions on your X account 2. Recipient has blocked you 3. Temporary platform issues **Diagnostic steps:** 1. Check the thread in the Inboxapp dashboard 2. Verify the message appears there 3. Try sending from the X app directly 4. Contact support if the issue persists *** ### Empty message error **Symptom:** `VALIDATION_ERROR` when sending a message. **Solution:** ```typescript theme={null} function validateMessage(text: string) { if (!text?.trim()) { throw new Error('Message cannot be empty'); } if (text.length > 10000) { throw new Error('Message exceeds 10,000 character limit'); } } // Always validate before sending validateMessage(messageText); await client.post(`/threads/${threadId}/messages`, { text: messageText }); ``` *** ## Filter issues ### Filters not working **Symptom:** Filters return unexpected results. **Common causes:** 1. Wrong parameter format 2. Incorrect filter combination **Solutions:** ```typescript theme={null} // Correct filter format (nested bracket notation) const { data } = await client.get('/threads', { params: { 'filters[tags][selectedIds][0]': 't8rvk2m5j0xn4wq7ybftcael', 'filters[tags][selectedIds][1]': 'u9swl3n6k1yo5xr8zcgudbfm' } }); // Wrong: using incorrect key structure const { data } = await client.get('/threads', { params: { 'filter[tagIds][0]': 't8rvk2m5j0xn4wq7ybftcael' // Wrong key! } }); ``` See the [Query Parameters guide](/reference/query-parameters) for encoding rules. *** ## Data sync issues ### Stale prospect data **Symptom:** Prospect profile data (followers, bio) seems outdated. **Explanation:** Prospect profile data is synced periodically from the platform, not in real-time. **Solutions:** 1. Profile data updates on each interaction 2. For critical decisions, fetch directly from X API 3. Don't rely on real-time accuracy for follower counts *** ### Missing threads **Symptom:** Conversations visible in Inboxapp UI don't appear in API. **Possible causes:** 1. Pagination not complete 2. Filtering out results 3. Different inbox views **Diagnostic steps:** ```typescript theme={null} // Check all inbox views const views = ['default', 'no-reply', 'requests', 'archived']; for (const view of views) { const { data } = await client.get('/threads', { params: { inbox: view, limit: 1 } }); console.log(`${view}: ${data.threads.length} threads`); } ``` *** ## Quick diagnostic script Run this to verify your setup: ```typescript theme={null} async function diagnose() { console.log('=== Inboxapp API Diagnostic ===\n'); // 1. Check authentication console.log('1. Testing authentication...'); try { const { data: team } = await client.get('/team'); console.log(` ✓ Connected to team: ${team.name}`); } catch (error) { console.log(` ✗ Auth failed: ${error.message}`); return; } // 2. Check account links console.log('\n2. Checking account links...'); const { data: accountLinks } = await client.get('/account-links'); console.log(` ✓ ${accountLinks.length} account(s) connected`); accountLinks.forEach((a: any) => { console.log(` - @${a.username} (${a.id})`); }); // 3. Check threads console.log('\n3. Checking threads...'); const { data: threads } = await client.get('/threads', { params: { limit: 5 } }); console.log(` ✓ ${threads.threads.length} recent threads`); // 4. Check rate limit status console.log('\n4. Rate limit status...'); // Make a request and check headers const response = await client.get('/team'); const remaining = response.headers['x-ratelimit-remaining']; const limit = response.headers['x-ratelimit-limit']; console.log(` ✓ ${remaining}/${limit} requests remaining`); console.log('\n=== Diagnostic Complete ==='); } diagnose().catch(console.error); ``` *** ## Getting help If you're still stuck: 1. **Check the error code** in the [Error Codes reference](/reference/error-codes) 2. **Review the API reference** for the specific endpoint 3. **Start a chat** with the button in the bottom-right corner, or browse the [Help Center](/help) 4. **Email** [kevin@inboxapp.com](mailto:kevin@inboxapp.com) for urgent issues When contacting support, include: * Error message and code * Request endpoint and parameters * Timestamp of the issue * Your team ID (from `/team` response) ## Related Complete error reference Rate limiting details Common questions Auth setup guide # campaign.completed Source: https://docs.inboxapp.com/webhooks/campaigns/campaign-completed Occurs when a campaign is completed ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 312, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "campaign.completed", "timestamp": "2025-01-20T08:00:00.000Z", "version": "1.0", "data": { "campaign": { "id": "n7kxqm5d9iu2sgdf6jbw4h36", "name": "Q1 Product Launch Outreach", "description": "Reach out to product managers about the new API release", "status": "completed", "dryRun": false, "targetsCount": 250, "contactedCount": 250, "repliesCount": 47, "completionRate": 1.0, "replyRate": 0.188, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-20T08:00:00.000Z", "pausedAt": null, "completedAt": "2025-01-20T08:00:00.000Z", "lastStartedAt": "2025-01-12T08:00:00.000Z" } } } ``` ## When it fires A campaign has finished contacting all of its targets — all scheduled messages (including follow-ups) have been sent. The campaign's `status` transitions to `"completed"` and `completedAt` is set. Use this event to trigger post-campaign workflows: generate performance reports, sync final metrics to your CRM, or notify your team that outreach is complete. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. # campaign.created Source: https://docs.inboxapp.com/webhooks/campaigns/campaign-created Occurs when a new campaign is created ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 301, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "campaign.created", "timestamp": "2025-01-15T09:00:00.000Z", "version": "1.0", "data": { "campaign": { "id": "m4wq7ybftcaelp8rvk2m5j0x", "name": "Q1 Product Launch Outreach", "description": "Reach out to design leads about the new collaboration features", "status": "draft", "dryRun": false, "targetsCount": 250, "contactedCount": 0, "repliesCount": 0, "completionRate": 0, "replyRate": 0, "createdAt": "2025-01-15T09:00:00.000Z", "updatedAt": null, "pausedAt": null, "completedAt": null, "lastStartedAt": null } } } ``` ## When it fires A new campaign is created in the workspace through the Inboxapp UI. Campaigns are created in `draft` status — they don't start sending messages until explicitly started. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. # campaign.paused Source: https://docs.inboxapp.com/webhooks/campaigns/campaign-paused Occurs when a campaign is paused ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 312, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "campaign.paused", "timestamp": "2025-01-16T09:15:00.000Z", "version": "1.0", "data": { "campaign": { "id": "x9wq5p2bvnhfdteolyr3km7j", "name": "Q1 Agency Outreach", "description": "Reaching out to design agencies in the Bay Area", "status": "paused", "dryRun": false, "targetsCount": 250, "contactedCount": 87, "repliesCount": 12, "completionRate": 0.348, "replyRate": 0.138, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-16T09:15:00.000Z", "pausedAt": "2025-01-16T09:15:00.000Z", "completedAt": null, "lastStartedAt": "2025-01-12T08:00:00.000Z" } } } ``` ## When it fires A running campaign is paused — either manually by a team member through the Inboxapp UI, or automatically by the system (e.g., if the connected account encounters an issue). While paused, the campaign stops contacting new targets and stops sending follow-up messages. Existing conversations remain active. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. # campaign.resumed Source: https://docs.inboxapp.com/webhooks/campaigns/campaign-resumed Occurs when a campaign is resumed ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 312, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "campaign.resumed", "timestamp": "2025-01-16T09:00:00.000Z", "version": "1.0", "data": { "campaign": { "id": "r3km7xj9wq5p2bvnhfdteoly", "name": "Q1 Product Launch Outreach", "description": "Reaching out to product managers about our new API features", "status": "active", "dryRun": false, "targetsCount": 500, "contactedCount": 230, "repliesCount": 34, "completionRate": 0.46, "replyRate": 0.148, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-16T09:00:00.000Z", "pausedAt": null, "completedAt": null, "lastStartedAt": "2025-01-16T09:00:00.000Z" } } } ``` ## When it fires A previously paused campaign is resumed — sending picks up where it left off. The campaign `status` transitions from `paused` back to `active`. Note that `pausedAt` is set to `null` and `lastStartedAt` is updated to the resume time. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. # campaign.started Source: https://docs.inboxapp.com/webhooks/campaigns/campaign-started Occurs when a campaign is started ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 312, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "campaign.started", "timestamp": "2025-01-16T09:00:00.000Z", "version": "1.0", "data": { "campaign": { "id": "w4h36qm5d9iu2sgdf6jbn7kx", "name": "Q1 Product Launch Outreach", "description": "Reaching out to design leaders about our new collaboration features", "status": "active", "dryRun": false, "targetsCount": 250, "contactedCount": 0, "repliesCount": 0, "completionRate": 0, "replyRate": 0, "createdAt": "2025-01-15T14:00:00.000Z", "updatedAt": "2025-01-16T09:00:00.000Z", "pausedAt": null, "completedAt": null, "lastStartedAt": "2025-01-16T09:00:00.000Z" } } } ``` ## When it fires * A campaign is started for the first time from the Inboxapp UI * A paused campaign is resumed (also triggers [`campaign.resumed`](/webhooks/campaigns/campaign-resumed)) When a campaign starts, its `status` transitions to `"active"` and `lastStartedAt` is set to the current timestamp. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. # message.created Source: https://docs.inboxapp.com/webhooks/messages/message-created Occurs when a message is sent or received ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 51, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.created", "timestamp": "2025-01-15T14:30:00.000Z", "version": "1.0", "data": { "message": { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "1876543210987654321", "userId": null, "campaignId": null, "content": "Hey! I saw your post about the new API. Would love to chat about integrating it with our platform.", "origin": "external", "createdAt": "2025-01-15T14:30:00.000Z", "updatedAt": null, "isEdited": false, "isEncrypted": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null, "deletedAt": null, "platformDeletedAt": null }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "lastMessageTimestamp": "2025-01-15T14:30:00.000Z", "computedSortTimestamp": "2025-01-15T14:30:00.000Z", "createdAt": "2025-01-10T09:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": true, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Hey! I saw your post about the new API. Would love to chat about integrating it with our platform.", "authorId": "1876543210987654321", "createdAt": "2025-01-15T14:30:00.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } } ``` ## When it fires * A prospect sends a DM to one of your connected accounts * A team member sends a message through the Inboxapp UI * A message is sent via the API * A campaign sends a message to a target Check the `origin` field to distinguish the source: `external` (from the platform), `internal` (from the Inboxapp UI), or `api` (from the API). Campaign messages include a `campaignId`. ## Payload The message object. The Inbox message ID. The platform the message belongs to (e.g., `twitter`). The platform's native message ID. The Inbox thread ID this message belongs to. The Inbox team ID. The author of the message. Either the `accountLinkId` (if sent by your team) or the prospect's `externalId`. The Inbox user ID of the team member who sent this message, if applicable. The campaign ID that sent this message, if applicable. The text content of the message. How the message was sent: `internal` (Inboxapp UI), `external` (synced from platform), or `api` (sent via API). When the message was created (ISO 8601). When the message was last updated (ISO 8601). Whether the message has been edited. Whether the message is end-to-end encrypted. Normalized URL entities, mentions, hashtags, and tweet shares. Normalized media and card attachments. Reactions on this message. Reply metadata if this message is a reply to another message. Forward metadata if this message was forwarded. When the message was soft-deleted in Inboxapp (ISO 8601). When the message was deleted on the platform (ISO 8601). The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. # message.deleted Source: https://docs.inboxapp.com/webhooks/messages/message-deleted Occurs when a message is deleted ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 104, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.deleted", "timestamp": "2025-01-15T16:00:00.000Z", "version": "1.0", "data": { "messageId": "p8rvk2m5j0xn4wq7ybftcael", "threadId": "l44e15irdq4db30i77cgphhx" } } ``` ## When it fires * A message is deleted through the Inboxapp UI * A message is deleted via the [Delete message](/api-reference/messages/delete-message) API endpoint * A message is deleted on the platform and the deletion is synced to Inboxapp ## Payload The ID of the deleted message. The ID of the thread the message belonged to. Inboxapp stores full message history, including deleted messages. You can still retrieve the original content using the [Get message history](/api-reference/messages/get-message-history) endpoint after receiving this event — no need to store message content preemptively. # message.edited Source: https://docs.inboxapp.com/webhooks/messages/message-edited Occurs when a message is edited ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 112, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.edited", "timestamp": "2025-01-15T15:10:00.000Z", "version": "1.0", "data": { "message": { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": "r3km7xj9wq5p2bvnhfdteoly", "campaignId": null, "content": "The updated proposal is attached — let me know if you have any questions about the pricing.", "origin": "internal", "createdAt": "2025-01-15T14:30:00.000Z", "updatedAt": "2025-01-15T15:10:00.000Z", "isEdited": true, "isEncrypted": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null, "deletedAt": null, "platformDeletedAt": null }, "threadId": "l44e15irdq4db30i77cgphhx" } } ``` ## When it fires A message's content is modified — either through the platform natively or via the [Edit message](/api-reference/messages/edit-message) API endpoint. The `message` object reflects the state **after** the edit. The `isEdited` field will be `true` and `updatedAt` will contain the edit timestamp. ## Payload The message object. The Inbox message ID. The platform the message belongs to (e.g., `twitter`). The platform's native message ID. The Inbox thread ID this message belongs to. The Inbox team ID. The author of the message. Either the `accountLinkId` (if sent by your team) or the prospect's `externalId`. The Inbox user ID of the team member who sent this message, if applicable. The campaign ID that sent this message, if applicable. The text content of the message. How the message was sent: `internal` (Inboxapp UI), `external` (synced from platform), or `api` (sent via API). When the message was created (ISO 8601). When the message was last updated (ISO 8601). Whether the message has been edited. Whether the message is end-to-end encrypted. Normalized URL entities, mentions, hashtags, and tweet shares. Normalized media and card attachments. Reactions on this message. Reply metadata if this message is a reply to another message. Forward metadata if this message was forwarded. When the message was soft-deleted in Inboxapp (ISO 8601). When the message was deleted on the platform (ISO 8601). The Inbox thread ID the message belongs to. Inboxapp stores full message history, including previous versions of edited messages. Use the [Get message history](/api-reference/messages/get-message-history) endpoint to retrieve the original content and all prior edits. # message.reactionAdded Source: https://docs.inboxapp.com/webhooks/messages/message-reaction-added Occurs when a reaction is added to a message ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 134, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.reactionAdded", "timestamp": "2025-01-15T15:45:00.000Z", "version": "1.0", "data": { "messageId": "p8rvk2m5j0xn4wq7ybftcael", "threadId": "l44e15irdq4db30i77cgphhx", "reaction": { "emoji": "🔥", "senderPlatformId": "1876543210987654321", "createdAt": "2025-01-15T15:45:00.000Z" } } } ``` A reaction was added to a message in a thread — either by a prospect on the platform or by a team member through Inboxapp. ## Payload The Inbox ID of the message that was reacted to. The Inbox thread ID containing the message. The reaction that was added or removed. The emoji character of the reaction. The platform ID of the user who reacted. ISO 8601 timestamp of when the reaction was created. # message.reactionRemoved Source: https://docs.inboxapp.com/webhooks/messages/message-reaction-removed Occurs when a reaction is removed from a message ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 156, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.reactionRemoved", "timestamp": "2025-01-15T14:55:00.000Z", "version": "1.0", "data": { "messageId": "p8rvk2m5j0xn4wq7ybftcael", "threadId": "l44e15irdq4db30i77cgphhx", "reaction": { "emoji": "👍", "senderPlatformId": "1876543210987654321", "createdAt": "2025-01-15T14:50:00.000Z" } } } ``` A reaction was removed from a message in a thread. ## Payload The Inbox ID of the message the reaction was removed from. The Inbox thread ID containing the message. The reaction that was added or removed. The emoji character of the reaction. The platform ID of the user who reacted. ISO 8601 timestamp of when the reaction was created. # Migrating from legacy webhooks Source: https://docs.inboxapp.com/webhooks/migrating-from-legacy Update your webhook integration to the new event format ## Overview We've expanded our webhook system with many new event types and a new event schema. This guide covers how to migrate from legacy webhooks to the new format. Legacy webhooks only supported three campaign target events. The new system adds support for threads, messages, prospects, tags, statuses, and campaigns—all using a consistent dot notation format. ## What's changed ### Event types Legacy webhooks only supported three events in camelCase format: * `targetContacted` * `targetFollowUpSent` * `targetReplied` The new system uses dot notation (`resource.action`) and adds many more event types. See the [full event reference](#event-reference) below. ### Payload schema The payload structure has changed significantly. The legacy format uses a flat structure with an `external` field, while the new format uses nested objects with a `prospect` field and additional context. **Legacy format:** ```json theme={null} { "event": "targetContacted", "version": "1.0", "timestamp": "2025-01-15T12:00:00.000Z", "platform": "twitter", "data": { "team": { ... }, "accountLink": { ... }, "message": { ... }, "external": { "id": "hzcai5t59nn9vsck3rbuepyg", "platformId": "1954360649393295360", "platform": "twitter", "displayName": "John Doe", "username": "johndoe", "image": "https://pbs.twimg.com/...", "data": { } }, "thread": { ... }, "campaign": { ... } } } ``` **New format:** ```json theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 42, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "target.contacted", "timestamp": "2025-01-15T12:00:00.000Z", "version": "1.0", "data": { "campaign": { ... }, "thread": { ... }, "message": { ... }, "prospect": { "id": "hzcai5t59nn9vsck3rbuepyg", "platformId": "1954360649393295360", "platform": "twitter", "displayName": "John Doe", "username": "johndoe", "image": "https://pbs.twimg.com/...", "context": { "tags": [...], "status": { ... }, "notes": "...", "valuation": 1000 } } } } ``` Key differences: * The `external` field is now `prospect` and includes full context (tags, status, notes, valuation) * New envelope fields: `id`, `seq`, `teamId` at the top level * The `event` field is now `type` * The `platform` field has moved from the top level into the data objects ### Retry behavior Legacy webhooks used aggressive retry logic (up to 10 retries over 24 hours). The new system attempts delivery once. Use the Events API to recover any missed events. ### Events API A new endpoint is available to replay missed events from the last 7 days: ```bash theme={null} GET /api/v1/events?afterSeq=0&limit=100 ``` **Parameters:** * `afterSeq` - Return events after this sequence number (omit to start from the beginning) * `limit` - Maximum events to return (default: 100, max: 1000) **Response:** ```json theme={null} { "events": [ { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 1, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "target.contacted", "timestamp": "2025-01-15T12:00:00.000Z", "version": "1.0", "data": { ... } } ], "hasMore": true, "lastSeq": 100 } ``` Use `lastSeq` as the `afterSeq` parameter for subsequent requests to paginate through all events. ## Migration steps ### 1. update your webhook handler Since the schema is different, you'll need to update your handler to process the new format. You can switch over at any time—disabling "Use legacy events" in your webhook configuration will immediately start sending the new format. ```javascript theme={null} function handleEvent(event) { // New format if (event.type === "target.contacted") { const { campaign, thread, message, prospect } = event.data; // prospect.context contains tags, status, notes, valuation } } ``` ### 2. implement missed event recovery Add a background job that periodically polls the Events API to catch any missed webhooks: ```javascript theme={null} let lastSeq = await getStoredSequenceNumber(); // Persist this value async function syncMissedEvents() { const response = await fetch( `https://api.inboxapp.com/v1/events?afterSeq=${lastSeq}&limit=1000`, { headers: { Authorization: `Bearer ${API_TOKEN}` } }, ); const { events, hasMore, lastSeq: newSeq } = await response.json(); for (const event of events) { await processEvent(event); // Deduplicate using event.id } if (newSeq) { lastSeq = newSeq; await storeSequenceNumber(lastSeq); } if (hasMore) { await syncMissedEvents(); // Continue pagination } } ``` ### 3. update webhook configuration Once your handler supports the new format, go to **Settings → Webhooks** and disable the **"Use legacy events"** option on your webhook configuration. ### 4. subscribe to new event types With the new system, you can now subscribe to many more event types beyond the original three. Update your webhook configuration to include any additional events you want to receive. ## Event reference All available event types in the new system: **Thread events** * `thread.created` - A new thread was created * `thread.deleted` - A thread was deleted * `thread.assigned` - A thread was assigned to a team member (**deprecated** — use `prospect.assigneeChanged`) * `thread.unassigned` - A thread was unassigned from a team member (**deprecated** — use `prospect.assigneeChanged`) * `thread.archived` - A thread was archived * `thread.unarchived` - A thread was unarchived * `thread.typing` - Someone is typing in a thread **Message events** * `message.created` - A new message was sent or received * `message.edited` - A message was edited * `message.deleted` - A message was deleted * `message.reactionAdded` - A reaction was added to a message * `message.reactionRemoved` - A reaction was removed from a message **Prospect events** * `prospect.created` - A new prospect was added * `prospect.statusChanged` - A prospect's pipeline status changed * `prospect.tagsChanged` - A prospect's tags were updated * `prospect.notesChanged` - A prospect's notes were updated * `prospect.valuationChanged` - A prospect's valuation was updated * `prospect.enriched` - A prospect was enriched with additional data * `prospect.assigneeChanged` - A prospect's assignee was set, changed, or cleared **Tag events** * `tag.created` - A new tag was created * `tag.updated` - A tag was updated * `tag.deleted` - A tag was deleted **Status events** * `status.created` - A new pipeline status was created * `status.updated` - A pipeline status was updated * `status.deleted` - A pipeline status was deleted **Campaign events** * `campaign.created` - A new campaign was created * `campaign.started` - A campaign was started * `campaign.paused` - A campaign was paused * `campaign.resumed` - A campaign was resumed * `campaign.completed` - A campaign completed **Target events** (previously the only events available) * `target.contacted` - A prospect was first contacted by a campaign (previously `targetContacted`) * `target.followUpSent` - A follow-up message was sent to a prospect (previously `targetFollowUpSent`) * `target.replied` - A prospect replied to a campaign message (previously `targetReplied`) ## Need help? If you have questions about migrating your webhooks, reach out to us at [support@inboxapp.com](mailto:support@inboxapp.com). # Webhooks overview Source: https://docs.inboxapp.com/webhooks/overview Get notified in real time when events happen in your Inboxapp workspace ## How webhooks work Webhooks send HTTP POST requests to your server when events occur in Inboxapp — a new message arrives, a thread is assigned, a campaign target replies, and more. Instead of polling the API for changes, register a webhook URL and Inboxapp pushes events to you as they happen. ``` Your server ◀── POST /your-webhook ── Inboxapp ``` Configure webhooks in **Settings → Webhooks** in your Inboxapp dashboard. You choose which event types each webhook receives. ## Event envelope Every webhook delivery wraps event-specific data in a consistent envelope: Unique event ID. Use this to deduplicate deliveries. Monotonically increasing sequence number per team. Use this to detect missed events and maintain ordering. The team this event belongs to. The event type in `resource.action` format (e.g., `thread.created`, `message.created`). ISO 8601 timestamp of when the event occurred. Schema version. Currently always `"1.0"`. The event payload. Structure varies by event type — see individual event pages for details. ```json Example envelope theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 42, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.created", "timestamp": "2025-01-15T10:30:00.000Z", "version": "1.0", "data": { "thread": { "..." }, "prospect": { "..." } } } ``` ## Supported event types Inboxapp supports 33 event types across 7 categories. Subscribe to individual events or entire categories. ### Thread events | Event | Description | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | [`thread.created`](/webhooks/threads/thread-created) | A new thread is created | | [`thread.deleted`](/webhooks/threads/thread-deleted) | A thread is deleted | | [`thread.assigned`](/webhooks/threads/thread-assigned) | A thread is assigned to a team member. **Deprecated** — use [`prospect.assigneeChanged`](/webhooks/prospects/prospect-assignee-changed) instead. | | [`thread.unassigned`](/webhooks/threads/thread-unassigned) | A thread is unassigned from a team member. **Deprecated** — use [`prospect.assigneeChanged`](/webhooks/prospects/prospect-assignee-changed) instead. | | [`thread.archived`](/webhooks/threads/thread-archived) | A thread is marked as done | | [`thread.unarchived`](/webhooks/threads/thread-unarchived) | A thread is reopened | | [`thread.typing`](/webhooks/threads/thread-typing) | A prospect is typing in an X Chat thread | ### Message events | Event | Description | | ------------------------------------------------------------------------ | ------------------------------------ | | [`message.created`](/webhooks/messages/message-created) | A message is sent or received | | [`message.edited`](/webhooks/messages/message-edited) | A message is edited | | [`message.deleted`](/webhooks/messages/message-deleted) | A message is deleted | | [`message.reactionAdded`](/webhooks/messages/message-reaction-added) | A reaction is added to a message | | [`message.reactionRemoved`](/webhooks/messages/message-reaction-removed) | A reaction is removed from a message | ### Prospect events | Event | Description | | ----------------------------------------------------------------------------- | ------------------------------------------------- | | [`prospect.created`](/webhooks/prospects/prospect-created) | A new prospect is created | | [`prospect.statusChanged`](/webhooks/prospects/prospect-status-changed) | A prospect's pipeline status changes | | [`prospect.tagsChanged`](/webhooks/prospects/prospect-tags-changed) | Tags are added or removed from a prospect | | [`prospect.notesChanged`](/webhooks/prospects/prospect-notes-changed) | A prospect's notes are updated | | [`prospect.valuationChanged`](/webhooks/prospects/prospect-valuation-changed) | A prospect's valuation changes | | [`prospect.assigneeChanged`](/webhooks/prospects/prospect-assignee-changed) | A prospect's assignee is set, changed, or cleared | | [`prospect.enriched`](/webhooks/prospects/prospect-enriched) | A prospect is enriched with fresh platform data | ### Tag events | Event | Description | | ------------------------------------------- | -------------------- | | [`tag.created`](/webhooks/tags/tag-created) | A new tag is created | | [`tag.updated`](/webhooks/tags/tag-updated) | A tag is updated | | [`tag.deleted`](/webhooks/tags/tag-deleted) | A tag is deleted | ### Status events | Event | Description | | ----------------------------------------------------- | -------------------------------- | | [`status.created`](/webhooks/statuses/status-created) | A new pipeline status is created | | [`status.updated`](/webhooks/statuses/status-updated) | A pipeline status is updated | | [`status.deleted`](/webhooks/statuses/status-deleted) | A pipeline status is deleted | ### Campaign events | Event | Description | | -------------------------------------------------------------- | ------------------------- | | [`campaign.created`](/webhooks/campaigns/campaign-created) | A new campaign is created | | [`campaign.started`](/webhooks/campaigns/campaign-started) | A campaign is started | | [`campaign.paused`](/webhooks/campaigns/campaign-paused) | A campaign is paused | | [`campaign.resumed`](/webhooks/campaigns/campaign-resumed) | A campaign is resumed | | [`campaign.completed`](/webhooks/campaigns/campaign-completed) | A campaign completes | ### Target events | Event | Description | | ---------------------------------------------------------------- | ---------------------------------------------- | | [`target.contacted`](/webhooks/targets/target-contacted) | A campaign target receives the initial message | | [`target.followUpSent`](/webhooks/targets/target-follow-up-sent) | A follow-up message is sent to a target | | [`target.replied`](/webhooks/targets/target-replied) | A campaign target replies | ## Delivery behavior Inboxapp attempts delivery **once** with no retries. Responses are not checked — delivery is fire-and-forget. Use the [Events API](#events-api) to fetch any events your webhook missed. Events are retained for 7 days. ### Handling webhooks Your endpoint should: 1. Deduplicate using `event.id` in case of rare duplicate deliveries 2. Handle events idempotently 3. Track the latest `seq` value so you can backfill missed events via the Events API ```typescript handler.ts theme={null} import express from 'express'; const app = express(); app.use(express.json()); app.post('/webhooks/inbox', async (req, res) => { const event = req.body; // Return 200 immediately res.sendStatus(200); // Process asynchronously switch (event.type) { case 'message.created': await handleNewMessage(event.data); break; case 'prospect.assigneeChanged': await handleAssigneeChanged(event.data); break; case 'prospect.statusChanged': await syncProspectToCRM(event.data); break; } }); app.listen(3000); ``` ```javascript handler.js theme={null} const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/inbox', async (req, res) => { const event = req.body; // Return 200 immediately res.sendStatus(200); // Process asynchronously switch (event.type) { case 'message.created': await handleNewMessage(event.data); break; case 'prospect.assigneeChanged': await handleAssigneeChanged(event.data); break; case 'prospect.statusChanged': await syncProspectToCRM(event.data); break; } }); app.listen(3000); ``` ## Events API The Events API lets you replay events from the last 7 days. Use it to recover missed webhooks or backfill data. ```bash cURL theme={null} curl "https://inboxapp.com/api/v1/events?afterSeq=0&limit=100" \ -H "Authorization: Bearer $INBOX_API_TOKEN" ``` **Response:** ```json theme={null} { "events": [ { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 1, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "message.created", "timestamp": "2025-01-15T10:30:00.000Z", "version": "1.0", "data": { } } ], "hasMore": true, "lastSeq": 100 } ``` | Parameter | Type | Description | | ---------- | -------- | --------------------------------------------------------------------------- | | `afterSeq` | `number` | Return events after this sequence number. Omit to start from the beginning. | | `limit` | `number` | Maximum events to return. Default: `100`, max: `1000`. | Use `lastSeq` from the response as `afterSeq` in your next request to paginate through all events. Store the last processed `seq` value persistently. Run a background job that periodically polls the Events API using your stored `seq` to catch anything your webhook handler missed. ### Missed event recovery ```typescript sync-events.ts theme={null} import axios from 'axios'; const client = axios.create({ baseURL: 'https://inboxapp.com/api/v1', headers: { 'Authorization': `Bearer ${process.env.INBOX_API_TOKEN}`, }, }); async function syncMissedEvents(lastSeq: number): Promise { let afterSeq = lastSeq; while (true) { const { data } = await client.get('/events', { params: { afterSeq, limit: 1000 }, }); for (const event of data.events) { await processEvent(event); // Deduplicate using event.id } if (!data.hasMore) { return data.lastSeq ?? afterSeq; } afterSeq = data.lastSeq; } } ``` ```javascript sync-events.js theme={null} const axios = require('axios'); const client = axios.create({ baseURL: 'https://inboxapp.com/api/v1', headers: { 'Authorization': `Bearer ${process.env.INBOX_API_TOKEN}`, }, }); async function syncMissedEvents(lastSeq) { let afterSeq = lastSeq; while (true) { const { data } = await client.get('/events', { params: { afterSeq, limit: 1000 }, }); for (const event of data.events) { await processEvent(event); // Deduplicate using event.id } if (!data.hasMore) { return data.lastSeq ?? afterSeq; } afterSeq = data.lastSeq; } } ``` ## Next steps Get notified about new, assigned, and archived threads Track messages, edits, and reactions Monitor campaign lifecycle changes Update from the old event format # prospect.assigneeChanged Source: https://docs.inboxapp.com/webhooks/prospects/prospect-assignee-changed Occurs when a prospect's assignee is changed, set, or cleared ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 210, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.assigneeChanged", "timestamp": "2025-01-15T14:20:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T14:20:00.000Z", "lastEnrichedAt": "2025-01-14T12:00:00.000Z", "lastActiveAt": "2025-01-15T11:00:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } ``` ## When it fires * A prospect is assigned to a team member through the Inboxapp UI or via the [Update prospect context](/api-reference/prospects/update-prospect-context) API endpoint * A prospect's assignment changes from one member to another * A prospect's assignee is cleared (set to `null`) The `context.assigneeId` field reflects the value **after** the change. This is the canonical event for assignee changes. The [`thread.assigned`](/webhooks/threads/thread-assigned) and [`thread.unassigned`](/webhooks/threads/thread-unassigned) events are deprecated in favor of this event since assignees are tracked at the prospect level, not the thread level. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.created Source: https://docs.inboxapp.com/webhooks/prospects/prospect-created Occurs when a new prospect is created ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 73, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.created", "timestamp": "2025-01-15T11:00:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "p8rvk2m5j0xn4wq7ybftcael", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-15T11:00:00.000Z", "lastUpdatedAt": "2025-01-15T11:00:00.000Z", "lastEnrichedAt": "2025-01-15T11:00:00.000Z", "lastActiveAt": "2025-01-14T22:15:00.000Z", "source": "live", "isFresh": true, "isStale": false, "confidence": 1.0, "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } } } ``` ## When it fires * A prospect sends you a DM for the first time * A new thread is created with a prospect that doesn't exist in your workspace yet ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.enriched Source: https://docs.inboxapp.com/webhooks/prospects/prospect-enriched Occurs when a prospect is enriched with additional data ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 201, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.enriched", "timestamp": "2025-01-15T16:00:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 13200, "followingCount": 905, "postCount": 3580, "engagementCount": 9340, "listedCount": 52, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T16:00:00.000Z", "lastEnrichedAt": "2025-01-15T16:00:00.000Z", "lastActiveAt": "2025-01-15T15:30:00.000Z", "source": "live", "isFresh": true, "isStale": false, "confidence": 1.0, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } ``` ## When it fires Inboxapp fetched fresh profile data from the platform for this prospect. Enrichment updates metrics like follower count, bio, profile image, and other platform-specific fields. The `lastEnrichedAt` timestamp reflects when this enrichment occurred. Compare the `confidence` score to gauge data freshness — `1.0` indicates real-time data. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.notesChanged Source: https://docs.inboxapp.com/webhooks/prospects/prospect-notes-changed Occurs when a prospect's notes are updated ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 201, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.notesChanged", "timestamp": "2025-01-15T16:15:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T16:15:00.000Z", "lastEnrichedAt": "2025-01-14T12:00:00.000Z", "lastActiveAt": "2025-01-15T11:00:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan. Scheduled demo for next Tuesday. Decision maker — reports directly to CTO." } } } } ``` ## When it fires A prospect's notes field is updated — either through the Inboxapp UI or via the [Update prospect context](/api-reference/prospects/update-prospect-context) API endpoint. The `prospect.context.notes` field contains the full notes content after the update. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.statusChanged Source: https://docs.inboxapp.com/webhooks/prospects/prospect-status-changed Occurs when a prospect's pipeline status is changed ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 203, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.statusChanged", "timestamp": "2025-01-15T16:20:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "p8rvk2m5j0xn4wq7ybftcael", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T16:20:00.000Z", "lastEnrichedAt": "2025-01-14T12:00:00.000Z", "lastActiveAt": "2025-01-15T11:30:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "n5xr8yk0wr6q3cwohgeufqmz", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } ``` ## When it fires * A team member changes a prospect's pipeline status through the Inboxapp UI * A prospect's status is updated via the [Update prospect context](/api-reference/prospects/update-prospect-context) API endpoint The `context.statusId` field reflects the **new** status. A value of `null` means the status was cleared. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.tagsChanged Source: https://docs.inboxapp.com/webhooks/prospects/prospect-tags-changed Occurs when tags are added or removed from a prospect ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 203, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.tagsChanged", "timestamp": "2025-01-15T16:45:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T16:45:00.000Z", "lastEnrichedAt": "2025-01-14T12:00:00.000Z", "lastActiveAt": "2025-01-15T11:30:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": [ "l44e15irdq4db30i77cgphhx", "r3km7xj9wq5p2bvnhfdteoly", "x9wq5p2bvnhfdteolyr3km7j" ], "statusId": "n5xr8yk0wr6q3cwohgeufqmz", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } ``` ## When it fires * A tag is added to a prospect through the Inboxapp UI or the API * A tag is removed from a prospect * Multiple tags are added or removed in a single operation The `context.tags` array in the payload reflects the **current** set of tag IDs after the change. To determine which tags were added or removed, compare against the previous state from your system. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # prospect.valuationChanged Source: https://docs.inboxapp.com/webhooks/prospects/prospect-valuation-changed Occurs when a prospect's valuation is changed ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 203, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "prospect.valuationChanged", "timestamp": "2025-01-15T16:30:00.000Z", "version": "1.0", "data": { "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-10T09:00:00.000Z", "lastUpdatedAt": "2025-01-15T16:30:00.000Z", "lastEnrichedAt": "2025-01-14T12:00:00.000Z", "lastActiveAt": "2025-01-15T11:00:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 15000, "notes": "Enterprise deal — upgraded from starter tier" } } } } ``` ## When it fires * A prospect's deal valuation is set or updated through the Inboxapp UI * A prospect's valuation is changed via the [Update prospect context](/api-reference/prospects/update-prospect-context) API endpoint * A prospect's valuation is cleared (set to `null`) The `context.valuation` field reflects the value **after** the change. ## Payload The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # status.created Source: https://docs.inboxapp.com/webhooks/statuses/status-created Occurs when a new pipeline status is created ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 88, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "status.created", "timestamp": "2025-01-15T11:00:00.000Z", "version": "1.0", "data": { "status": { "id": "n5xr8yk0wr6q3cwohgeufqmz", "name": "Qualified", "color": "#10b981", "createdAt": "2025-01-15T11:00:00.000Z" } } } ``` ## When it fires A new pipeline status is created in the workspace through the Inboxapp UI. Statuses represent stages in your sales pipeline (e.g., "New Lead", "Warm", "Qualified", "Closed Won"). ## Payload The status object. Unique identifier for the status. The display name of the status (e.g., "New Lead", "Qualified", "Closed Won"). Hex color code for the status (e.g., `#10b981`). Null if no color is set. ISO 8601 timestamp of when the status was created. # status.deleted Source: https://docs.inboxapp.com/webhooks/statuses/status-deleted Occurs when a pipeline status is deleted ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 189, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "status.deleted", "timestamp": "2025-01-15T17:00:00.000Z", "version": "1.0", "data": { "statusId": "n5xr8yk0wr6q3cwohgeufqmz" } } ``` ## When it fires A pipeline status is permanently deleted from the workspace — either through the Inboxapp UI or via the [Delete status](/api-reference/statuses/delete-status) API endpoint. Prospects that were assigned this status will have their `context.statusId` set to `null`. If your integration relies on status data, handle this gracefully. ## Payload The ID of the deleted status. # status.updated Source: https://docs.inboxapp.com/webhooks/statuses/status-updated Occurs when a pipeline status is updated ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 189, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "status.updated", "timestamp": "2025-01-15T17:10:00.000Z", "version": "1.0", "data": { "status": { "id": "n5xr8yk0wr6q3cwohgeufqmz", "name": "Qualified Lead", "color": "#3b82f6", "createdAt": "2025-01-02T09:00:00.000Z" } } } ``` ## When it fires * A pipeline status is renamed through the Inboxapp UI * A status color is changed * Any status property is modified The `status` object reflects the state **after** the update. ## Payload The status object. Unique identifier for the status. The display name of the status (e.g., "New Lead", "Qualified", "Closed Won"). Hex color code for the status (e.g., `#10b981`). Null if no color is set. ISO 8601 timestamp of when the status was created. # tag.created Source: https://docs.inboxapp.com/webhooks/tags/tag-created Occurs when a new tag is created ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 88, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "tag.created", "timestamp": "2025-01-15T11:00:00.000Z", "version": "1.0", "data": { "tag": { "id": "l44e15irdq4db30i77cgphhx", "name": "High Priority", "color": "#ef4444" } } } ``` ## When it fires A new tag is created in the workspace — either through the Inboxapp UI or via the [Create tag](/api-reference/tags/create-tag) API endpoint. ## Payload The tag object. Unique identifier for the tag. The display name of the tag. Hex color code for the tag (e.g., `#3b82f6`). Null if no color is set. # tag.deleted Source: https://docs.inboxapp.com/webhooks/tags/tag-deleted Occurs when a tag is deleted ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 89, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "tag.deleted", "timestamp": "2025-01-15T17:00:00.000Z", "version": "1.0", "data": { "tagId": "l44e15irdq4db30i77cgphhx" } } ``` ## When it fires A tag is permanently deleted from the workspace. Any prospects that had this tag will have it removed from their `context.tags` array. Deleting a tag removes it from all prospects. If you're syncing tags to an external system, use this event to clean up references to the deleted tag. ## Payload The ID of the deleted tag. # tag.updated Source: https://docs.inboxapp.com/webhooks/tags/tag-updated Occurs when a tag is updated ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 178, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "tag.updated", "timestamp": "2025-01-15T15:30:00.000Z", "version": "1.0", "data": { "tag": { "id": "l44e15irdq4db30i77cgphhx", "name": "Enterprise Lead", "color": "#8b5cf6" } } } ``` ## When it fires * A tag's name is changed through the Inboxapp UI * A tag's color is changed * A tag is updated via the [Update tag](/api-reference/tags/update-tag) API endpoint The `tag` object reflects the state **after** the update. ## Payload The tag object. Unique identifier for the tag. The display name of the tag. Hex color code for the tag (e.g., `#3b82f6`). Null if no color is set. # target.contacted Source: https://docs.inboxapp.com/webhooks/targets/target-contacted Occurs when a campaign target receives the initial message ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 401, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "target.contacted", "timestamp": "2025-01-16T10:05:00.000Z", "version": "1.0", "data": { "campaign": { "id": "m4wq7ybftcaelp8rvk2m5j0x", "name": "Q1 Product Launch Outreach", "description": "Reaching out to design leaders about our new collaboration features", "status": "active", "dryRun": false, "targetsCount": 250, "contactedCount": 88, "repliesCount": 12, "completionRate": 0.352, "replyRate": 0.136, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-16T10:05:00.000Z", "pausedAt": null, "completedAt": null, "lastStartedAt": "2025-01-12T08:00:00.000Z" }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": null, "lastMessageTimestamp": "2025-01-16T10:05:00.000Z", "computedSortTimestamp": "2025-01-16T10:05:00.000Z", "createdAt": "2025-01-16T10:05:00.000Z", "status": "active", "variant": "unencrypted", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Hi Sarah — I noticed your work on the Figma design system. We just launched a collaboration API that might be a great fit for your team. Would love to chat!", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "createdAt": "2025-01-16T10:05:00.000Z", "userId": null, "campaignId": "m4wq7ybftcaelp8rvk2m5j0x" }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } }, "message": { "id": "p8rvk2m5j0xn4wq7ybftcael", "platform": "twitter", "platformId": "1876543210987654322", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": null, "campaignId": "m4wq7ybftcaelp8rvk2m5j0x", "content": "Hi Sarah — I noticed your work on the Figma design system. We just launched a collaboration API that might be a great fit for your team. Would love to chat!", "origin": "internal", "createdAt": "2025-01-16T10:05:00.000Z", "updatedAt": null, "isEdited": false, "isEncrypted": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null, "deletedAt": null, "platformDeletedAt": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-15T10:00:00.000Z", "lastUpdatedAt": "2025-01-16T10:05:00.000Z", "lastEnrichedAt": "2025-01-15T10:00:00.000Z", "lastActiveAt": "2025-01-16T08:30:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } } } ``` ## When it fires A campaign sends the **initial message** to a target. This is the first point of contact — the opening DM in the campaign sequence. This event fires once per target, when the first step of the campaign reaches them. Subsequent follow-up messages trigger [`target.followUpSent`](/webhooks/targets/target-follow-up-sent) instead. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. The message object. The Inbox message ID. The platform the message belongs to (e.g., `twitter`). The platform's native message ID. The Inbox thread ID this message belongs to. The Inbox team ID. The author of the message. Either the `accountLinkId` (if sent by your team) or the prospect's `externalId`. The Inbox user ID of the team member who sent this message, if applicable. The campaign ID that sent this message, if applicable. The text content of the message. How the message was sent: `internal` (Inboxapp UI), `external` (synced from platform), or `api` (sent via API). When the message was created (ISO 8601). When the message was last updated (ISO 8601). Whether the message has been edited. Whether the message is end-to-end encrypted. Normalized URL entities, mentions, hashtags, and tweet shares. Normalized media and card attachments. Reactions on this message. Reply metadata if this message is a reply to another message. Forward metadata if this message was forwarded. When the message was soft-deleted in Inboxapp (ISO 8601). When the message was deleted on the platform (ISO 8601). The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # target.followUpSent Source: https://docs.inboxapp.com/webhooks/targets/target-follow-up-sent Occurs when a follow-up message is sent to a campaign target ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 421, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "target.followUpSent", "timestamp": "2025-01-18T09:00:00.000Z", "version": "1.0", "data": { "campaign": { "id": "m4wq7ybftcaelp8rvk2m5j0x", "name": "Q1 Product Launch Outreach", "description": "Reach out to design leads about the new collaboration features", "status": "active", "dryRun": false, "targetsCount": 250, "contactedCount": 180, "repliesCount": 29, "completionRate": 0.72, "replyRate": 0.161, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-18T09:00:00.000Z", "pausedAt": null, "completedAt": null, "lastStartedAt": "2025-01-12T08:00:00.000Z" }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": null, "lastMessageTimestamp": "2025-01-18T09:00:00.000Z", "computedSortTimestamp": "2025-01-18T09:00:00.000Z", "createdAt": "2025-01-16T09:05:00.000Z", "status": "active", "variant": "unencrypted", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "w7xn4kp8r2mv6qjtybftcael", "content": "Just wanted to follow up on my previous message — would you be open to a quick 15-minute call this week?", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "createdAt": "2025-01-18T09:00:00.000Z", "userId": null, "campaignId": "m4wq7ybftcaelp8rvk2m5j0x" }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } }, "message": { "id": "w7xn4kp8r2mv6qjtybftcael", "platform": "twitter", "platformId": "1876543210987654399", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "userId": null, "campaignId": "m4wq7ybftcaelp8rvk2m5j0x", "content": "Just wanted to follow up on my previous message — would you be open to a quick 15-minute call this week?", "origin": "internal", "createdAt": "2025-01-18T09:00:00.000Z", "updatedAt": null, "isEdited": false, "isEncrypted": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null, "deletedAt": null, "platformDeletedAt": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-16T09:05:00.000Z", "lastUpdatedAt": "2025-01-18T09:00:00.000Z", "lastEnrichedAt": "2025-01-16T09:05:00.000Z", "lastActiveAt": "2025-01-17T22:30:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } }, "stepNumber": 2 } } ``` ## When it fires A follow-up message in a campaign sequence is sent to a target who hasn't replied to a previous message. Campaigns can have multiple follow-up steps — the `stepNumber` field indicates which step was sent. This event is only fired for follow-up messages (step 2+). The initial outreach message triggers [`target.contacted`](/webhooks/targets/target-contacted) instead. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. The message object. The Inbox message ID. The platform the message belongs to (e.g., `twitter`). The platform's native message ID. The Inbox thread ID this message belongs to. The Inbox team ID. The author of the message. Either the `accountLinkId` (if sent by your team) or the prospect's `externalId`. The Inbox user ID of the team member who sent this message, if applicable. The campaign ID that sent this message, if applicable. The text content of the message. How the message was sent: `internal` (Inboxapp UI), `external` (synced from platform), or `api` (sent via API). When the message was created (ISO 8601). When the message was last updated (ISO 8601). Whether the message has been edited. Whether the message is end-to-end encrypted. Normalized URL entities, mentions, hashtags, and tweet shares. Normalized media and card attachments. Reactions on this message. Reply metadata if this message is a reply to another message. Forward metadata if this message was forwarded. When the message was soft-deleted in Inboxapp (ISO 8601). When the message was deleted on the platform (ISO 8601). The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. The step number in the campaign sequence (2 for the first follow-up, 3 for the second, etc.). # target.replied Source: https://docs.inboxapp.com/webhooks/targets/target-replied Occurs when a campaign target replies ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 487, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "target.replied", "timestamp": "2025-01-17T16:45:00.000Z", "version": "1.0", "data": { "campaign": { "id": "m4wq7ybftcaelp8rvk2m5j0x", "name": "Q1 Product Launch Outreach", "description": "Reach out to product managers about the new API release", "status": "active", "dryRun": false, "targetsCount": 250, "contactedCount": 142, "repliesCount": 28, "completionRate": 0.568, "replyRate": 0.197, "createdAt": "2025-01-10T09:00:00.000Z", "updatedAt": "2025-01-17T16:45:00.000Z", "pausedAt": null, "completedAt": null, "lastStartedAt": "2025-01-12T08:00:00.000Z" }, "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "lastMessageTimestamp": "2025-01-17T16:45:00.000Z", "computedSortTimestamp": "2025-01-17T16:45:00.000Z", "createdAt": "2025-01-16T10:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "w7xn4kp8r2mv6qjtnb3ky9sz", "content": "This looks interesting! We've been looking for exactly this kind of integration. Can we schedule a call this week?", "authorId": "1876543210987654321", "createdAt": "2025-01-17T16:45:00.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } }, "message": { "id": "w7xn4kp8r2mv6qjtnb3ky9sz", "platform": "twitter", "platformId": "1876543210987654399", "threadId": "l44e15irdq4db30i77cgphhx", "teamId": "hzcai5t59nn9vsck3rbuepyg", "authorId": "1876543210987654321", "userId": null, "campaignId": null, "content": "This looks interesting! We've been looking for exactly this kind of integration. Can we schedule a call this week?", "origin": "external", "createdAt": "2025-01-17T16:45:00.000Z", "updatedAt": null, "isEdited": false, "isEncrypted": false, "entities": null, "attachment": null, "reactions": [], "replyData": null, "forwardData": null, "deletedAt": null, "platformDeletedAt": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "imageNormalized": "https://pbs.twimg.com/profile_images/example/photo.jpg", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-16T10:00:00.000Z", "lastUpdatedAt": "2025-01-17T16:45:00.000Z", "lastEnrichedAt": "2025-01-16T10:00:00.000Z", "lastActiveAt": "2025-01-17T16:45:00.000Z", "source": "cached", "isFresh": true, "isStale": false, "confidence": 0.95, "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } ``` ## When it fires A prospect replies to a campaign message — either the initial outreach or a follow-up. This is the key conversion event for campaign analytics. The `message` object contains the prospect's reply. The `campaign` object reflects updated `repliesCount` and `replyRate` metrics at the time the reply was received. Use this event to trigger high-priority workflows — notify your sales team, move the prospect to a "warm" pipeline status, or route the thread to the right team member. ## Payload The campaign object. The Inbox campaign ID. The campaign name. The campaign description. The campaign status. One of `draft`, `active`, `paused`, or `completed`. Whether the campaign is in dry run mode (no messages actually sent). Total number of targets in the campaign. Number of targets that have been contacted. Number of targets that have replied. Percentage of targets contacted (0–1). Percentage of contacted targets that replied (0–1). ISO 8601 timestamp of when the campaign was created. ISO 8601 timestamp of the last update, or `null`. ISO 8601 timestamp of when the campaign was paused, or `null`. ISO 8601 timestamp of when the campaign completed, or `null`. ISO 8601 timestamp of when the campaign was last started, or `null`. The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. The message object. The Inbox message ID. The platform the message belongs to (e.g., `twitter`). The platform's native message ID. The Inbox thread ID this message belongs to. The Inbox team ID. The author of the message. Either the `accountLinkId` (if sent by your team) or the prospect's `externalId`. The Inbox user ID of the team member who sent this message, if applicable. The campaign ID that sent this message, if applicable. The text content of the message. How the message was sent: `internal` (Inboxapp UI), `external` (synced from platform), or `api` (sent via API). When the message was created (ISO 8601). When the message was last updated (ISO 8601). Whether the message has been edited. Whether the message is end-to-end encrypted. Normalized URL entities, mentions, hashtags, and tweet shares. Normalized media and card attachments. Reactions on this message. Reply metadata if this message is a reply to another message. Forward metadata if this message was forwarded. When the message was soft-deleted in Inboxapp (ISO 8601). When the message was deleted on the platform (ISO 8601). The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # thread.archived Source: https://docs.inboxapp.com/webhooks/threads/thread-archived Occurs when a thread is marked as done A thread was marked as done (archived), either through the Inboxapp UI or the API. Archived threads move out of the active inbox view. ## Payload The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 67, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.archived", "timestamp": "2025-01-15T14:20:00.000Z", "version": "1.0", "data": { "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": true, "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "lastMessageTimestamp": "2025-01-15T14:10:00.000Z", "computedSortTimestamp": "2025-01-15T14:10:00.000Z", "createdAt": "2025-01-10T09:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": true, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Sounds good, I'll send over the proposal tomorrow.", "authorId": "df6jbw4h36qm5d9iu2sgn7kx", "createdAt": "2025-01-15T14:10:00.000Z", "userId": "r3km7xj9wq5p2bvnhfdteoly", "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "m4wq7ybftcaelp8rvk2m5j0x", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/sarah.jpg", "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "n5xr8yk0wr6q3cwohgeufqmz", "valuation": 2500, "notes": "Closing deal — proposal sent" } } } } } ``` # thread.assigned Source: https://docs.inboxapp.com/webhooks/threads/thread-assigned Occurs when a thread is assigned to a team member **Deprecated.** Assignees are now managed at the prospect level. Use [`prospect.assigneeChanged`](/webhooks/prospects/prospect-assignee-changed) instead. This event will be removed in a future version. ## When it fires * A thread is assigned to a team member through the Inboxapp UI * A thread is assigned via the API * A thread's assignment changes from one member to another Removing an assignee fires [`thread.unassigned`](/webhooks/threads/thread-unassigned) instead. ## Payload The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 19, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.assigned", "timestamp": "2025-01-15T14:20:00.000Z", "version": "1.0", "data": { "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "lastMessageTimestamp": "2025-01-15T14:15:00.000Z", "computedSortTimestamp": "2025-01-15T14:15:00.000Z", "createdAt": "2025-01-14T09:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Can you send over the pricing details?", "authorId": "1876543210987654321", "createdAt": "2025-01-15T14:15:00.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } } ``` # thread.created Source: https://docs.inboxapp.com/webhooks/threads/thread-created Occurs when a new thread is created ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 42, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.created", "timestamp": "2025-01-15T10:30:00.000Z", "version": "1.0", "data": { "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": null, "lastMessageTimestamp": null, "computedSortTimestamp": "2025-01-15T10:30:00.000Z", "createdAt": "2025-01-15T10:30:00.000Z", "status": "active", "variant": "unencrypted", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": null, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "p8rvk2m5j0xn4wq7ybftcael", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/...", "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "p8rvk2m5j0xn4wq7ybftcael", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/...", "imageNormalized": "https://pbs.twimg.com/profile_images/...", "bio": "Product designer at Figma. Building tools for the next generation of creators.", "location": "San Francisco, CA", "profileUrl": "https://x.com/sarahchen", "websiteUrl": "https://sarahchen.design", "websiteDomain": "sarahchen.design", "verified": "none", "profileType": "personal", "isProtected": false, "followerCount": 12840, "followingCount": 892, "postCount": 3420, "engagementCount": 8912, "listedCount": 47, "platformCreatedAt": "2019-03-15T08:00:00.000Z", "firstSeenAt": "2025-01-15T10:30:00.000Z", "lastUpdatedAt": "2025-01-15T10:30:00.000Z", "lastEnrichedAt": "2025-01-15T10:30:00.000Z", "lastActiveAt": "2025-01-14T22:15:00.000Z", "source": "live", "isFresh": true, "isStale": false, "confidence": 1.0, "context": { "tags": [], "statusId": null, "valuation": null, "notes": null } } } } ``` ## When it fires Triggered when a new DM thread is created — through the Inboxapp UI, via the API, or when a prospect messages you for the first time. ## Payload The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. The prospect associated with this event, including Inboxapp context. The social platform. Currently `"twitter"`. The platform's native user ID (e.g. `"1566123362161725440"`). The Inbox prospect ID. Null if the prospect hasn't been fully imported. Display name as shown on their profile. Platform username without the `@` symbol. Username with `@` prefix (e.g. `"@acmecorp"`). Profile picture URL as provided by the platform. Profile picture URL normalized to highest quality. Prefer this over `image` for display. Profile bio/description text. Freeform location string as entered by the user. Direct link to the profile on the platform. External website URL from their profile. Domain extracted from `websiteUrl`. Verification status: `"none"`, `"verified"`, `"business"`, or `"government"`. Account type: `"personal"`, `"business"`, or `"government"`. Whether the account is private/protected. Number of followers. Number of accounts this user follows. Total posts/tweets by this account. Platform-specific engagement metric (e.g. likes given on X). Number of X lists this profile appears on. ISO 8601 timestamp of when the account was created on the platform. ISO 8601 timestamp of when this prospect was first added to Inboxapp. ISO 8601 timestamp of the most recent data update. ISO 8601 timestamp of when fresh data was last fetched from the platform. ISO 8601 timestamp of last platform activity. Data source: `"cached"`, `"indexed"`, `"live"`, or `"merged"`. Whether data was updated within the last 7 days. Whether data hasn't been updated in over 30 days. Data accuracy confidence score from `0` to `1`. Inboxapp-specific context for this prospect. Array of tag IDs attached to this prospect. The pipeline status ID, or `null` if unset. The prospect's deal valuation, or `null` if unset. Free-text notes attached to the prospect. # thread.deleted Source: https://docs.inboxapp.com/webhooks/threads/thread-deleted Occurs when a thread is deleted ## When it fires A thread is permanently deleted from the workspace — either through the Inboxapp UI or the [Delete thread](/api-reference/threads/delete-thread) API endpoint. ## Payload The ID of the deleted thread. ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 43, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.deleted", "timestamp": "2025-01-15T10:31:00.000Z", "version": "1.0", "data": { "threadId": "l44e15irdq4db30i77cgphhx" } } ``` Only the `threadId` is included — the full thread object is no longer available after deletion. If you need thread data for your integration, store it when you receive `thread.created` events. # thread.typing Source: https://docs.inboxapp.com/webhooks/threads/thread-typing Occurs when a prospect is typing in an X Chat thread ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 47, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.typing", "timestamp": "2025-01-15T10:30:00.000Z", "version": "1.0", "data": { "threadId": "l44e15irdq4db30i77cgphhx", "senderPlatformId": "1876543210987654321" } } ``` ## When it fires A prospect is actively typing in a thread. This event is transient — it indicates ongoing input, not a completed message. Only supported for **X Chat** threads (where `variant` is `"xChat"`). Threads must have both parties exchanging messages before they transition to X Chat. You won't receive this event for unencrypted threads. This is a lightweight event. Unlike other thread events, it only includes the thread ID and the sender's platform ID — not the full thread or prospect objects. ## Payload The Inbox thread ID where typing is occurring. The platform ID of the prospect who is typing. # thread.unarchived Source: https://docs.inboxapp.com/webhooks/threads/thread-unarchived Occurs when a thread is reopened A thread that was previously marked as done is reopened — either through the Inboxapp UI, the API, or when a new message arrives in an archived thread. ## Payload The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 89, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.unarchived", "timestamp": "2025-01-15T14:10:00.000Z", "version": "1.0", "data": { "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": "r3km7xj9wq5p2bvnhfdteoly", "lastMessageTimestamp": "2025-01-15T14:09:55.000Z", "computedSortTimestamp": "2025-01-15T14:09:55.000Z", "createdAt": "2025-01-10T09:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": true, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Actually, one more question about the pricing tiers.", "authorId": "l44e15irdq4db30i77cgphhx", "createdAt": "2025-01-15T14:09:55.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "l44e15irdq4db30i77cgphhx", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": ["n7kx5d9iu2sgdf6jbw4h36qm"], "statusId": "w4h36qm5d9iu2sgdf6jbn7kx", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } } ``` # thread.unassigned Source: https://docs.inboxapp.com/webhooks/threads/thread-unassigned Occurs when a thread is unassigned from a team member **Deprecated.** Use [`prospect.assigneeChanged`](/webhooks/prospects/prospect-assignee-changed) instead. Assignees are now managed at the prospect level, so this event will stop firing in a future release. ## When it fires * A thread's assignee is removed through the Inboxapp UI * A thread is unassigned via the API by setting `assigneeId` to `null` ## Payload The thread associated with this event. The Inbox thread ID. The platform this thread belongs to. Currently `"twitter"`. The platform conversation ID. Whether the thread has been archived. The ID of the member assigned to the thread. ISO 8601 timestamp of the last message in the thread. Timestamp used for sorting — last message time or creation date if no messages exist. ISO 8601 timestamp of when the thread was created. `"active"` or `"idle"`. Idle threads are hidden from the main inbox. `"unencrypted"` or `"xChat"`. Transitions to `"xChat"` once both parties have exchanged messages. The ID of the account link (connected X account) associated with this thread. Whether the thread is currently syncing message history from the platform. Whether this conversation is a message request. Whether typing indicators are enabled. `null` uses the team default. The most recent message in the thread. The Inbox message ID. The message text content. The `accountLinkId` or prospect's `externalId`, depending on who sent the message. ISO 8601 timestamp. The Inbox user ID of the sender, if sent by a team member. The campaign ID, if sent by a campaign. The prospect associated with this thread. See the [Prospect object](/webhooks/overview#prospect-object) for the full shape. ```json Example payload theme={null} { "id": "ck9v2m5nj0xp4wq7ybftrae8", "seq": 20, "teamId": "hzcai5t59nn9vsck3rbuepyg", "type": "thread.unassigned", "timestamp": "2025-01-15T14:25:00.000Z", "version": "1.0", "data": { "thread": { "id": "l44e15irdq4db30i77cgphhx", "platform": "twitter", "platformId": "1566123362161725440:1876543210987654321", "done": false, "assigneeId": null, "lastMessageTimestamp": "2025-01-15T14:15:00.000Z", "computedSortTimestamp": "2025-01-15T14:15:00.000Z", "createdAt": "2025-01-14T09:00:00.000Z", "status": "active", "variant": "xChat", "accountLinkId": "df6jbw4h36qm5d9iu2sgn7kx", "isSyncing": false, "isRequest": false, "typingIndicatorsEnabled": null, "lastMessage": { "id": "p8rvk2m5j0xn4wq7ybftcael", "content": "Can you send over the pricing details?", "authorId": "1876543210987654321", "createdAt": "2025-01-15T14:15:00.000Z", "userId": null, "campaignId": null }, "prospect": { "platform": "twitter", "platformId": "1876543210987654321", "externalId": "n7kxqm5d9iu2sgdf6jbw4h36", "displayName": "Sarah Chen", "username": "sarahchen", "handle": "@sarahchen", "image": "https://pbs.twimg.com/profile_images/example/photo.jpg", "context": { "tags": ["l44e15irdq4db30i77cgphhx"], "statusId": "x9wq5p2bvnhfdteolyr3km7j", "valuation": 2500, "notes": "Interested in enterprise plan" } } } } } ``` # Verifying webhook signatures Source: https://docs.inboxapp.com/webhooks/verifying-signatures Validate that webhook events are authentically sent by Inboxapp ## Why verify signatures Without verification, any server that discovers your webhook URL could send fake events. Inboxapp signs every webhook delivery with your signing secret so you can confirm it's authentic before processing. ## How it works Every webhook request includes an `X-Inbox-Signature` header with a timestamp and HMAC-SHA256 signature: ```ts theme={null} "X-Inbox-Signature": "t=1705312200,v1=a1b2c3d4e5f6..." ``` The signature is computed over the timestamp and raw request body joined by a dot: ```ts theme={null} HMAC_SHA256(secret, "{timestamp}.{raw_json_body}") ``` This format prevents replay attacks — the timestamp is part of the signed payload, so an attacker can't reuse a captured signature with a different body or at a different time. ## Getting your signing secret 1. Go to **Settings → Webhooks** in your Inboxapp dashboard 2. Click on a webhook configuration 3. Copy the **Signing secret** Store it securely as an environment variable: ```bash theme={null} export INBOX_WEBHOOK_SECRET="ibt_wh_your_signing_secret_here" ``` ## Verification steps ### Extract the timestamp and signature Parse the `X-Inbox-Signature` header to get the `t` (timestamp) and `v1` (signature) values. ### Reconstruct the signed payload Concatenate the timestamp, a `.` character, and the **raw request body** (before any JSON parsing): `{timestamp}.{raw_body}` ### Compute the expected signature Generate an HMAC-SHA256 hash of the signed payload using your signing secret. ### Compare signatures Use a constant-time comparison to check if the computed signature matches the `v1` value from the header. ### Validate the timestamp Check that the timestamp is within an acceptable tolerance (e.g., 5 minutes) to prevent replay attacks. ## Code examples ```typescript verify.ts theme={null} import { createHmac, timingSafeEqual } from "crypto"; const TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes interface VerificationResult { valid: boolean; reason?: string; } function verifyWebhookSignature( rawBody: string, signatureHeader: string, secret: string, ): VerificationResult { // 1. Parse the signature header const parts = Object.fromEntries( signatureHeader.split(",").map((part) => { const [key, ...rest] = part.split("="); return [key, rest.join("=")]; }), ); const timestamp = parts["t"]; const signature = parts["v1"]; if (!timestamp || !signature) { return { valid: false, reason: "Missing timestamp or signature" }; } // 2. Validate the timestamp const eventTime = parseInt(timestamp, 10); const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - eventTime) > TIMESTAMP_TOLERANCE_SECONDS) { return { valid: false, reason: "Timestamp outside tolerance" }; } // 3. Compute the expected signature const signedPayload = `${timestamp}.${rawBody}`; const expected = createHmac("sha256", secret) .update(signedPayload) .digest("hex"); // 4. Compare using constant-time comparison const expectedBuffer = Buffer.from(expected, "hex"); const receivedBuffer = Buffer.from(signature, "hex"); if (expectedBuffer.length !== receivedBuffer.length) { return { valid: false, reason: "Invalid signature" }; } if (!timingSafeEqual(expectedBuffer, receivedBuffer)) { return { valid: false, reason: "Invalid signature" }; } return { valid: true }; } ``` ```javascript verify.js theme={null} const { createHmac, timingSafeEqual } = require("crypto"); const TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes function verifyWebhookSignature(rawBody, signatureHeader, secret) { // 1. Parse the signature header const parts = Object.fromEntries( signatureHeader.split(",").map((part) => { const [key, ...rest] = part.split("="); return [key, rest.join("=")]; }), ); const timestamp = parts["t"]; const signature = parts["v1"]; if (!timestamp || !signature) return false; // 2. Validate the timestamp const eventTime = parseInt(timestamp, 10); const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - eventTime) > TIMESTAMP_TOLERANCE_SECONDS) { return false; } // 3. Compute the expected signature const signedPayload = `${timestamp}.${rawBody}`; const expected = createHmac("sha256", secret) .update(signedPayload) .digest("hex"); // 4. Compare using constant-time comparison const expectedBuffer = Buffer.from(expected, "hex"); const receivedBuffer = Buffer.from(signature, "hex"); if (expectedBuffer.length !== receivedBuffer.length) return false; return timingSafeEqual(expectedBuffer, receivedBuffer); } ``` ```python verify.py theme={null} import hmac import hashlib import time TIMESTAMP_TOLERANCE_SECONDS = 300 # 5 minutes def verify_webhook_signature(raw_body: str, signature_header: str, secret: str) -> bool: # 1. Parse the signature header parts = dict( part.split("=", 1) for part in signature_header.split(",") ) timestamp = parts.get("t") signature = parts.get("v1") if not timestamp or not signature: return False # 2. Validate the timestamp event_time = int(timestamp) current_time = int(time.time()) if abs(current_time - event_time) > TIMESTAMP_TOLERANCE_SECONDS: return False # 3. Compute the expected signature signed_payload = f"{timestamp}.{raw_body}" expected = hmac.new( secret.encode("utf-8"), signed_payload.encode("utf-8"), hashlib.sha256, ).hexdigest() # 4. Compare using constant-time comparison return hmac.compare_digest(expected, signature) ``` ## Full handler example ```typescript handler.ts theme={null} import express from "express"; const app = express(); // Important: use raw body for signature verification app.use("/webhooks/inbox", express.raw({ type: "application/json" })); app.post("/webhooks/inbox", (req, res) => { const signature = req.headers["x-inbox-signature"] as string; const rawBody = req.body.toString(); if (!signature) { return res.status(401).json({ error: "Missing signature" }); } const result = verifyWebhookSignature( rawBody, signature, process.env.INBOX_WEBHOOK_SECRET!, ); if (!result.valid) { return res.status(401).json({ error: result.reason }); } const event = JSON.parse(rawBody); // Process the verified event console.log("Verified event:", event.type, event.id); res.sendStatus(200); }); app.listen(3000); ``` ```python handler.py theme={null} from flask import Flask, request, jsonify import json import os app = Flask(__name__) @app.route("/webhooks/inbox", methods=["POST"]) def handle_webhook(): signature = request.headers.get("X-Inbox-Signature") raw_body = request.get_data(as_text=True) if not signature: return jsonify({"error": "Missing signature"}), 401 if not verify_webhook_signature( raw_body, signature, os.environ["INBOX_WEBHOOK_SECRET"], ): return jsonify({"error": "Invalid signature"}), 401 event = json.loads(raw_body) # Process the verified event print(f"Verified event: {event['type']} {event['id']}") return "", 200 ``` Make sure you verify against the **raw request body** string, not a re-serialized version. Parsing the JSON and re-serializing it may change whitespace or key ordering, which will produce a different signature. ## Common mistakes | Mistake | Fix | | ---------------------------------- | ------------------------------------------------------------------------ | | Parsing JSON before verifying | Use the raw body string for verification, then parse after | | Using non-constant-time comparison | Always use `timingSafeEqual` (Node.js) or `hmac.compare_digest` (Python) | | Not checking the timestamp | Always validate that `t` is within your tolerance window | | Hardcoding the secret | Store it in an environment variable or secret manager | ## Rotating your signing secret You can rotate your signing secret at any time from **Settings → Webhooks**. When you rotate: 1. The old secret is immediately invalidated 2. All subsequent deliveries use the new secret 3. Update your verification code with the new secret before rotating, or accept a brief window of failed verifications