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 Inbox dashboard under Settings → Billing, or contact support@inboxapp.com for help.
Standard send (existing thread)
Send a message in a thread you’ve already created or looked up:
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);
Response:
{
"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:
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);
Response:
{
"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:
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}`);
});
Response:
{
"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:
// 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 Inbox |
"internal" | Sent from the Inbox UI or a campaign |
"api" | Sent via the Inbox API |
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.
For long conversations, paginate through messages using cursorId and cursorTimestamp:
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 for streaming patterns and error handling.
Replying to a specific message
You can reply to a specific message by providing replyToMessageId:
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:
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:
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:
// 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:
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
// 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
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
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 for current limits and backoff strategies.