Inbox
List LinkedIn and email conversations with your leads, read message history, reply (with attachments or voice notes on LinkedIn), and react to replies programmatically.
The inbox holds the conversations your connected sender accounts and mailboxes have with your leads, in one place: LinkedIn chats with people who are leads in the workspace, and email threads between your mailboxes and your leads. Conversations with anyone else are not stored. Campaign steps, direct actions and manual messages all land in the same threads, so the inbox API is where you build reply handling: routing hot leads to a CRM, powering a custom unibox, or auto-responding to replies.
The chat and message model
A chat is one conversation thread: between a sender account and a lead on LinkedIn, or between one of your mailboxes and a lead by email. channel (linkedin or email) says which. A message is one entry inside a chat.
Key chat fields:
| Field | Type | Meaning |
|---|---|---|
id | string | The chat id used in every /inbox/chats/{id} call. |
accountId | string, null | The sender account (LinkedIn) or mailbox (email) on your side of the thread. |
channel | string | linkedin or email. |
subject | string, null | The email thread's subject; null on LinkedIn chats. |
attendee | object, null | Snapshot of the person on the other side: their LinkedIn profile fields, or name and email on an email thread. |
leadId | string, null | The workspace lead this person maps to, when tracked. |
lastMessage | object, null | A preview of the most recent message in the thread: text, at, isFromMe. |
lastMessageAt | date-time, null | When the thread last had activity. |
lastReplyAt | date-time, null | When the lead last messaged this thread. Null until they answer, and never cleared once set: your own replies do not reset it. |
unreadCount | integer | Unread messages in the thread. |
favorite, archived | boolean | Inbox flags. |
Key message fields:
| Field | Type | Meaning |
|---|---|---|
id, chatId | string | Identity and parent thread. |
channel | string | linkedin or email. |
subject, messageId, html | string, null | Email only: the subject, the RFC 5322 Message-ID, and the HTML body as received (text is the plain-text version). |
openedAt, clickedAt | date-time, null | Sent email only, when tracked: the first open and the first link click. |
text | string | The message body. |
isFromMe | boolean | true for messages sent from your side (the sender account or the mailbox). |
isEvent | boolean | true for system events rather than regular messages. |
attachments | array | Media on a LinkedIn message, sent or received: type (image, video, audio, file), url, name, mimeType, size. Voice notes carry voiceNote: true and durationMs. Email messages carry none for now. |
at | date-time | When the message was sent. |
List and filter chats
listInboxChats returns chats ordered by most recent activity, with standard limit (1 to 100, default 50) and offset paging.
The replied query parameter filters on whether the lead has answered:
replied=true: only threads where the lead has sent at least one message. These are ordered by the lead's most recent reply instead of general activity, so the freshest replies come first. Threads stored before reply tracking have no recorded reply time and sort last.replied=false: only threads still awaiting a first reply.- Omit it for all chats. Any other value is rejected with
400.
replied is the only filter: there is none by account, channel, lead or unread state, so filter on those fields of the returned chats yourself.
curl "https://app.swarmhit.com/api/v1/inbox/chats?replied=true&limit=25" \
-H "Authorization: Bearer swh_live_..."This is the polling-friendly view of "who answered me": page through replied=true and compare each chat's lastReplyAt against your own high-water mark. For push instead of polling, see reacting to replies below.
Read a conversation
Fetch a single thread with getInboxChat, and its history with listChatMessages:
curl "https://app.swarmhit.com/api/v1/inbox/chats/CHAT_ID/messages?limit=50" \
-H "Authorization: Bearer swh_live_..."Messages come newest first. Use isFromMe to render the two sides of the conversation and isEvent to filter out system entries.
Reading over the API never fetches from LinkedIn or the mailbox: it returns what Swarmhit has recorded, which includes the lead's replies and every message sent through Swarmhit.
For a lead-centric view of campaign activity rather than a thread, getLeadActivity returns a lead's campaign timeline across campaigns, newest first (capped at 200 events). It holds step results and events such as reply_received, not the inbox messages themselves.
Mark a chat read
markChatRead clears the unread count for the chat and for every sibling thread with the same person on the same channel (one person can have more than one thread). A LinkedIn chat is also marked read on LinkedIn; on an email thread the count is cleared in Swarmhit only, and the message's read state in the mailbox is left as it is:
curl -X POST "https://app.swarmhit.com/api/v1/inbox/chats/CHAT_ID/read" \
-H "Authorization: Bearer swh_live_..."Reply in a chat
sendChatMessage sends a message into an existing thread, from the chat's sender account (or, on an email thread, its mailbox):
curl -X POST "https://app.swarmhit.com/api/v1/inbox/chats/CHAT_ID/messages" \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f2c9a1e-7d31-4b8a-9f0e-2d6c8b5a1e3f" \
-d '{"text": "Thanks for connecting! Happy to share more."}'Always send an Idempotency-Key header (any unique string up to 255 characters, for example a UUID). Reusing the key on the same chat replays the first response instead of sending again, so a network retry cannot double-message a lead. A 409 with code: idempotency_in_progress means the original request is still in flight (retry shortly); code: idempotency_failed means the original errored after it may have taken effect (retry with a new key). A request rejected with a 4xx sent nothing and frees its key.
The response is { ok, messageId, campaignLead }. campaignLead is the chat's linked campaign membership (id and status) so your app can offer to pause or stop the sequence, or null when the chat has none. On an email thread, messageId is the sent email's RFC 5322 Message-ID.
A reply counts toward the sending account's daily usage like an automated send, but is never blocked by its daily limit. If a LinkedIn chat's sender account is gone or disconnected, the request is rejected with 400 and code: account-missing.
This endpoint is for existing threads. To start a brand new conversation with a connection, use the direct action sendDirectMessage instead. See Direct actions.
Replying on an email thread
On an email chat, the reply goes out from the chat's mailbox as a reply in the same thread: subject Re: <thread subject>, sent to whoever wrote last (out-of-office and other auto-replies are skipped; before any reply, to the lead's address), with the mailbox's signature appended and opens and clicks tracked per the mailbox's settings. It takes text only for now: attachments or a voiceMessage is rejected with 400. The send counts toward the workspace's email allowance and fires the email.sent webhook with source: manual.
Two email-specific errors, both 400: code: mailbox-not-ready when the chat's mailbox is not connected and active, and code: email-limit-reached while email is paused for the workspace (this billing period's allowance is used up, or a charge for email usage failed). workspace.emails on GET / shows what is left.
Attachments and voice notes
LinkedIn chats only. text becomes optional when you provide an attachment or a voice message. Attachments are fetched server-side from a public http(s) url that serves the file directly, since redirects are not followed (photo, video, audio or document, max 15 MB each):
curl -X POST "https://app.swarmhit.com/api/v1/inbox/chats/CHAT_ID/messages" \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b7d3e5f-1a24-4c6d-8e90-3f5a7c9b1d2e" \
-d '{
"text": "Here is the one-pager I mentioned.",
"attachments": [
{ "url": "https://example.com/files/one-pager.pdf", "name": "one-pager.pdf" }
]
}'voiceMessage sends a LinkedIn voice note: the audio at its url (up to 12 MB) is fetched and transcoded to LinkedIn's format server-side, and anything past 60 seconds is cut off. durationMs is optional; the measured length is used whenever it can be read.
Halting the sequence on send
If the chat's lead is running in a campaign, replying manually via the API does not interrupt the sequence by itself. Use sequenceAction to halt it on send. It acts on the membership the chat is linked to (the campaignLead in the response), on LinkedIn and email alike, and does nothing when there is none:
"pause": freezes the sequence, keeping step progress and timers, so it can be resumed later. A paused membership still holds this person for that sender in other campaigns, since it can resume."stop": terminal. The executor never contacts this lead again in that campaign, and any membership waiting behind it (same person, same sender) is released to start.- Omit it (or send
null) to leave the sequence running.
curl -X POST "https://app.swarmhit.com/api/v1/inbox/chats/CHAT_ID/messages" \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f" \
-d '{"text": "Great, let me send over some times.", "sequenceAction": "stop"}'You can also pause, resume or stop the lead independently of sending, via pauseLeadInCampaign, resumeLeadInCampaign and stopLeadInCampaign. In Swarmhit, the inbox does not pause the lead on its own either: after a manual message it offers to pause or stop the sequence. Over the API, sequenceAction is that choice.
React to replies programmatically
Polling replied=true works, but the push path is webhooks. Subscribe an endpoint to message.received (LinkedIn) and email.received (email) and Swarmhit calls you when a lead replies. message.received fires for replies from campaign leads and from leads you actioned through the API; email.received fires for replies from campaign leads, but not for out-of-office and other auto-replies:
curl -X POST "https://app.swarmhit.com/api/v1/webhooks" \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/swarmhit",
"events": ["message.received", "email.received"],
"classifyReplies": true
}'The message.received payload carries:
data.source:campaignwhen the reply is on a campaign lead,directwhen it is on a lead you actioned via the API.data.accountId: the sender account the reply arrived on.data.campaignIdanddata.campaignLeadId: set for campaign replies, null for direct ones.data.leadId: the lead who replied.data.chatId: the conversation's id on LinkedIn (on email, the thread key). It is not the inbox chatidthat the/inbox/chats/{id}endpoints take.data.message: the replytextand its timestamp.data.from: the replier'sfirstName,lastName,publicIdentifierandproviderId.
email.received has the same shape, with data.accountId being the mailbox, data.from.email (the lead's address, or the sender's address when the lead has none) in place of providerId, and an extra data.email block (subject, messageId).
A typical handler: find the inbox chat whose leadId and accountId match the event (listInboxChats with replied=true lists the freshest replies first), fetch its history via listChatMessages for context, send a reply with sendChatMessage, then markChatRead. The outbound mirror events, message.sent (LinkedIn, on chats with a tracked lead) and email.sent (campaign emails and inbox replies), let you keep both sides of the conversation in sync in your own system.
Deliveries are at-least-once: deduplicate on the X-Swarmhit-Delivery header and verify the X-Swarmhit-Signature HMAC. The Webhooks guide covers both.
Reply classification
With classifyReplies enabled on a webhook endpoint, every message.received and email.received delivery to it carries a classification.interest verdict: interested, not_interested or neutral, or null when no classification was produced (treat null as unknown, never as neutral). Classification consumes workspace credits. It only runs when an enabled endpoint subscribed to message.received has classifyReplies on (or the campaign has aiInterestEnabled), so an endpoint that wants verdicts on email replies should subscribe to message.received as well, as in the example above. The full contract, vocabulary and example payload live in the Webhooks guide.
To persist the verdict on the campaign membership, call setLeadInterest with interested or not_interested, or null to clear it (the stored interest field has no neutral value; its companion interestBy records whether it was set by manual action or ai). Campaigns can also do this themselves when aiInterestEnabled is on, without overriding an interest set manually.
Reference
List inbox chats
Page through conversations, with the replied filter.
List messages in a chat
Read a thread's history.
Send a message in a chat
Reply with text, attachments or a voice note; email threads take text.
Mark a chat read
Clear unread counts, on LinkedIn too for LinkedIn chats.
Webhooks guide
Subscribe to message.received and verify deliveries.