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.
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 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
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 externalIdconst 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 APIif (message.origin === 'api') { console.log('Sent via API');}// Sent by a campaignif (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.
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.
// Cache your account link IDs at startupconst { 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.
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.