# 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
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**.
2. Open **Statuses** to see your current stages.
3. Click **Add Status**, name it, and pick a color.
4. Drag and drop statuses to change their order in the pipeline.
5. Use the **⋯** menu on a status to edit or delete it.
## 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).
3. Open **Settings → General**.
4. Click **Invite**, enter their email, and click **Send Invites**.
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.
2. Open **Campaigns** → **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**.
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.
3. Select the recipient list and continue.
4. In the message editor, open the **Custom Variable** control (often shown as an asterisk or similar).
5. Choose a field such as **First Name**. Inboxapp substitutes each lead’s value at send time.
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).
3. Choose your lead list.
4. Set the send window / schedule. Prefer recipients’ daytime hours.
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)
**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.
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.
3. Open the **Overview** tab.
4. Review targeted contacts, replies, reply rate, completion rate, recent conversations, and any sentiment summary shown.
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.
2. Open the **Stages** tab (alongside Overview and Settings).
3. Review counts and drill into a stage for individual leads.
## 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**.
5. Enter the list details and start the 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.
# 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.
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.
## 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**.
2. Optionally filter by date, status, tag, source, or custom field.
3. Select specific leads, or leave selection empty to export the filtered set.
4. Click **Export**.
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