> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inboxapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with messages

> Sending and retrieving messages in the Inbox API

## Sending messages

Two ways to send messages: within an existing thread, or using quick send.

<Warning>
  **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 [Inbox dashboard](https://inboxapp.com) under **Settings → Billing**, or contact [support@inboxapp.com](mailto:support@inboxapp.com) for help.
</Warning>

### Standard send (existing thread)

Send a message in a thread you've already created or looked up:

<CodeGroup>
  ```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?"}'
  ```
</CodeGroup>

**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
}
```

<Note>
  The send message endpoint returns the `Message` object directly — not wrapped in `{ message: ... }`.
</Note>

### Quick send

Quick send combines thread lookup/creation and message sending in one request:

<CodeGroup>
  ```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."
    }'
  ```
</CodeGroup>

**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:

<CodeGroup>
  ```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"
  ```
</CodeGroup>

**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);
}
```

<Warning>
  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.
</Warning>

| `origin` value | Meaning                                                                          |
| -------------- | -------------------------------------------------------------------------------- |
| `"external"`   | Pulled from X — the message was discovered on the platform and synced into Inbox |
| `"internal"`   | Sent from the Inbox UI or a campaign                                             |
| `"api"`        | Sent via the Inbox API                                                           |

<Note>
  `origin` describes *where the message entered Inbox*, 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 Inbox UI.
</Note>

## 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);
```

<Warning>
  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.
</Warning>

<Note>
  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.
</Note>

## 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);
```

<Warning>
  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.
</Warning>

## 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!'
    });
  }
}
```

<Warning>
  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.
</Warning>

### 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;
}
```

<Note>
  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.
</Note>

## Related endpoints

<CardGroup cols={2}>
  <Card title="Send message" icon="send" href="/api-reference/messages/send-message">
    POST /threads/{threadId}/messages
  </Card>

  <Card title="Quick send" icon="zap" href="/api-reference/messages/quick-send-message">
    POST /threads/messages
  </Card>

  <Card title="List messages" icon="list-tree" href="/api-reference/messages/list-messages">
    GET /threads/{threadId}/messages
  </Card>

  <Card title="Managing threads" icon="messages-square" href="/guides/managing-threads">
    Thread operations and filtering
  </Card>
</CardGroup>
