Inbox
The Inbox API exposes Warmerly's unified inbox, every email and LinkedIn DM tied to a contact, in one place. Use it to list contacts, pull a full conversation timeline, and send replies across channels.
Inbox endpoints only need X-Api-Key. They do not read X-Project-Id, unlike most other resources, the inbox is scoped to a workspace, which can span multiple projects at once. The workspace is the one named by the X-Workspace-Id header (you must be a member), otherwise your own personal workspace. An API key has no browser selection to fall back on, so send X-Workspace-Id if your mailboxes live in a different workspace.
curl https://app.warmerly.com/api/v1/inbox/contacts \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"#List contacts
GET /inbox/contactsReturns the inbox contacts in your workspace, the people/companies you've exchanged messages with across email and LinkedIn, newest activity first. Archived conversations are left out unless you pass archived=true. Warmup traffic between Warmerly-managed mailboxes is never listed.
| Query param | Required | Description |
|---|---|---|
workspaceId | No | Filter to contacts belonging to a single project. Despite the name, this is a project ID (the inbox filter bar labels projects as "workspaces"). |
accountId | No | Filter to contacts with at least one message sent/received via this account (an email, LinkedIn or WhatsApp account in your workspace). An account or project that is not in your workspace returns an empty page, not an error. |
q | No | Search contact name, email, phone and company (substring match) and message content (subject, body, sender; each term matches by prefix). |
needsReply | No | true, only conversations whose newest message is inbound and is either classified as a lead or not classified yet. |
archived | No | true, only conversations where every message is archived. |
unread | No | true, only conversations with at least one unread inbound message. |
category | No | lead, not_relevant, or spam. Any other value is ignored. |
channel | No | email, linkedin, or whatsapp. Any other value is ignored. |
limit | No | Page size, 1-200 (default 50). Values outside the range are clamped. |
offset | No | Page offset (default 0). |
All filters are applied before pagination, so total is the real number of matching conversations and paging through with offset reaches every one of them.
curl "https://app.warmerly.com/api/v1/inbox/contacts?accountId=8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"contacts": [
{
"id": "c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b",
"name": "Jamie Rivera",
"company": "Northwind Traders",
"campaignName": "Q3 outreach",
"avatarUrl": null,
"email": "jamie@northwindtraders.com",
"phone": "+15551234567",
"linkedinProfileId": "jamierivera",
"linkedinProfileUrl": "https://www.linkedin.com/in/jamierivera",
"channels": ["email", "linkedin"],
"lastActivityAt": "2026-07-01T16:42:03.000Z",
"hasUnread": true,
"lastPreview": "Thanks for reaching out, can we do a call next week?",
"category": "lead",
"categoryReason": "Replied positively and asked to schedule a call",
"latestInboundMessageId": "9d1e0b7a-5e7b-4f10-b2aa-1c8f0d3e6a4b",
"latestDirection": "inbound",
"latestChannel": "email",
"latestInboundChannel": "email",
"isArchived": false
}
],
"total": 128,
"hasMore": true
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Contact ID, use this for the timeline and reply endpoints. |
name | string | Display name, falling back to email or phone if unset. |
company | string \| null | Company name, when known from an associated campaign lead. |
campaignName | string \| null | Name of that campaign lead's campaign, when there is one. |
email | string \| null | Contact's email address. |
phone | string \| null | Contact's phone number, when known. |
linkedinProfileId | string \| null | LinkedIn profile identifier, when known. |
linkedinProfileUrl | string \| null | LinkedIn profile URL, when known. |
channels | array | Channels (email, linkedin) this contact has any message on. |
lastActivityAt | string | ISO 8601 timestamp of the most recent message. A failed outbound send does not count. |
hasUnread | boolean | True if there's at least one unread inbound message. |
lastPreview | string \| null | Snippet of the most recent message. |
category | "lead" \| "not_relevant" \| "spam" \| null | AI/rule-assigned triage category. |
categoryReason | string \| null | Short explanation for the assigned category. |
latestInboundMessageId | string \| null | ID of the newest inbound message. |
latestDirection | "inbound" \| "outbound" \| null | Direction of the newest message (failed outbound sends ignored). |
latestChannel | "email" \| "linkedin" \| "whatsapp" \| null | Channel of the newest message. |
latestInboundChannel | "email" \| "linkedin" \| "whatsapp" \| null | Channel of the newest inbound message. |
isArchived | boolean | True if every message in the conversation is archived. |
#Get a contact's timeline
GET /inbox/contacts/{contactId}/timelineReturns the full conversation with a contact across all channels, in chronological order (the most recent 200 messages), plus any autosaved email draft in progress for the latest inbound thread. Reading a timeline marks its unread messages as read, as opening the conversation in the dashboard does. Add ?preview=1 to read without marking anything read.
curl https://app.warmerly.com/api/v1/inbox/contacts/c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b/timeline \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"items": [
{
"id": "9d1e0b7a-5e7b-4f10-b2aa-1c8f0d3e6a4b",
"channel": "email",
"direction": "inbound",
"status": "sent",
"content": "Hi, thanks for the intro, happy to learn more.",
"bodyHtml": "<p>Hi, thanks for the intro, happy to learn more.</p>",
"subject": "Re: Quick question about your workflow",
"sentAt": "2026-06-30T11:05:00.000Z",
"fromAddr": "jamie@northwindtraders.com",
"fromName": "Jamie Rivera",
"toAddrs": ["you@yourcompany.com"],
"externalId": null,
"error": null,
"hasAttachments": false,
"attachmentsMeta": null,
"isWarmup": false,
"sentBy": null
},
{
"id": "7f3a2cdb-9e1a-4f0c-8b6d-2e5f7a1c3b9d",
"channel": "email",
"direction": "outbound",
"status": "sent",
"content": "Great, does Tuesday at 3pm work for a quick call?",
"bodyHtml": null,
"subject": "Re: Quick question about your workflow",
"sentAt": "2026-07-01T16:42:03.000Z",
"fromAddr": "you@yourcompany.com",
"fromName": "Your Name",
"toAddrs": ["jamie@northwindtraders.com"],
"externalId": null,
"error": null,
"hasAttachments": false,
"attachmentsMeta": null,
"isWarmup": false,
"sentBy": null
}
],
"draft": null,
"latestSubject": "Re: Quick question about your workflow",
"replyAccountId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"contact": {
"id": "c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b",
"category": "lead",
"categoryReason": "Replied positively and asked to schedule a call"
}
}| Field | Type | Description |
|---|---|---|
items[].channel | "email" \| "linkedin" | Channel the message was sent/received on. |
items[].direction | "inbound" \| "outbound" | Who sent it. |
items[].status | "queued" \| "sent" \| "failed" | Delivery status. |
items[].content | string | Plain-text body (falls back to snippet if the full body wasn't stored). |
items[].hasAttachments | boolean | Whether the message has attachments. |
items[].attachmentsMeta | array \| null | Attachment details (filename, contentType, size, part), usable with the attachment download endpoint. |
items[].isWarmup | boolean | True for Warmerly warmup traffic. |
items[].sentBy | "autopilot" \| null | "autopilot" when LinkedIn AI Autopilot sent the message. |
draft | object \| null | Autosaved email draft for the latest inbound email thread, if one exists (subject, bodyText). |
latestSubject | string \| null | Subject of the most recent message that has one. |
replyAccountId | string \| null | The mailbox the email thread started on, which replies are sent from. |
If contactId doesn't exist, or has no message in your workspace, this returns 404:
{ "error": { "code": "not_found", "message": "not_found", "details": null } }#Reply to a contact
POST /inbox/contacts/{contactId}/replySends a reply to a contact on a specific channel and appends it to the timeline. Each channel has its own requirements:
| Field | Type | Required | Description |
|---|---|---|---|
channel | "email" \| "linkedin" | Yes | Channel to send on. |
body | string | Yes | Message text, at least 1 character. |
subject | string | Email only | Required when channel is "email". A Re: prefix is added if missing. |
sourceAccountId | string | Yes | ID of the connected account to send from (email account or LinkedIn account, matching channel). Must belong to your workspace, otherwise 400 invalid_source_account. |
curl -X POST https://app.warmerly.com/api/v1/inbox/contacts/c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b/reply \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"subject": "Re: Quick question about your workflow",
"body": "Great, does Tuesday at 3pm work for a quick call?",
"sourceAccountId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
}'{
"message": {
"id": "7f3a2cdb-9e1a-4f0c-8b6d-2e5f7a1c3b9d",
"channel": "email",
"direction": "outbound",
"status": "sent",
"contactId": "c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b",
"accountId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"subject": "Re: Quick question about your workflow",
"bodyText": "Great, does Tuesday at 3pm work for a quick call?",
"toAddrs": ["jamie@northwindtraders.com"],
"fromAddr": "you@yourcompany.com",
"receivedAt": "2026-07-01T16:42:03.000Z"
}
}message is the stored message row, so it carries more fields than shown here. For email, the stored bodyText is the text you sent plus your mailbox signature and the quoted message you replied to.
Notes on behaviour by channel:
- Email sends synchronously over SMTP; the contact must have an
emailon file andsubjectis required. If the contact already has an email thread in your workspace, the reply goes out from the mailbox that thread started on, even ifsourceAccountIdnames another of your mailboxes. A send failure returns500with the SMTP error message, but the failed message is still recorded in the timeline withstatus: "failed". - LinkedIn sends synchronously through your connected LinkedIn account; the contact must have a
linkedinProfileIdon file. A send failure returns500and is recorded asfailed, as for email. Some account types cannot send yet and return400(for examplenative_send_not_ready).
Missing channel-specific data returns 400 (contact_has_no_email, subject_required, contact_has_no_linkedin, contact_has_no_phone); a body that fails validation returns 400 invalid_body; and a contact that is not in your workspace returns 404. For example:
{ "error": { "code": "bad_request", "message": "contact_has_no_email", "details": null } }#List threads
GET /inbox/threadsReturns email threads (grouped by threadKey + accountId) with aggregate counts, for building a mailbox-style view. Only email mailboxes in your workspace are included. This is account/folder-oriented, as opposed to /inbox/contacts, which is contact-oriented and spans channels.
| Query param | Required | Description |
|---|---|---|
accountId | No | Filter to a single email account. An account that is not in your workspace returns 404 not_found. |
folder | No | IMAP folder to filter by. Defaults to INBOX. Ignored unless view is inbox. |
view | No | One of inbox (default), archived, starred, sent. Any other value applies no view filter. |
q | No | Full-text search across subject, sender, and body (Postgres FTS with an ILIKE fallback). |
limit | No | Max threads to return, 1 to 100. Defaults to 50. |
curl "https://app.warmerly.com/api/v1/inbox/threads?accountId=8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a&view=inbox&limit=20" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"threads": [
{
"threadKey": "<CADx...@mail.gmail.com>",
"accountId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"lastReceivedAt": "2026-07-01T16:42:03.000Z",
"messageCount": 4,
"unreadCount": 1,
"starred": false,
"lastFromAddr": "jamie@northwindtraders.com",
"lastFromName": "Jamie Rivera",
"lastSubject": "Re: Quick question about your workflow",
"lastSnippet": "Great, does Tuesday at 3pm work for a quick call?"
}
]
}#Get a thread
GET /inbox/threads/{key}Returns the raw messages in a single email thread, chronological order (the most recent 200). {key} is the threadKey from /inbox/threads (URL-encode it, thread keys are typically Message-Id-style strings and contain special characters).
| Query param | Required | Description |
|---|---|---|
accountId | Yes | The email account the thread is on. Missing: 400 bad_request. An account that is not in your workspace: 404 not_found. |
curl "https://app.warmerly.com/api/v1/inbox/threads/%3CCADx...%40mail.gmail.com%3E?accountId=8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"messages": [
{
"id": "9d1e0b7a-5e7b-4f10-b2aa-1c8f0d3e6a4b",
"accountId": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"folder": "INBOX",
"threadKey": "<CADx...@mail.gmail.com>",
"fromAddr": "jamie@northwindtraders.com",
"fromName": "Jamie Rivera",
"toAddrs": ["you@yourcompany.com"],
"subject": "Re: Quick question about your workflow",
"bodyText": "Hi, thanks for the intro, happy to learn more.",
"isRead": true,
"isStarred": false,
"isArchived": false,
"channel": "email",
"direction": "inbound",
"status": "sent",
"receivedAt": "2026-06-30T11:05:00.000Z"
}
]
}This endpoint returns raw inbox_messages rows rather than the trimmed shape used by the timeline endpoint, so each message carries more fields than shown here.
#Filter options
GET /inbox/filter-optionsReturns the projects and email accounts in your workspace that the inbox can be filtered by, for populating a workspace/account picker in your own UI.
curl https://app.warmerly.com/api/v1/inbox/filter-options \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"workspaces": [
{ "id": "b2f4a6c1-3e5d-4f7a-9b1c-0d2e4f6a8c1b", "name": "Marketing outreach" }
],
"accounts": [
{
"id": "8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a",
"email": "you@yourcompany.com",
"provider": "gmail",
"projectId": "b2f4a6c1-3e5d-4f7a-9b1c-0d2e4f6a8c1b"
}
]
}workspaces here is a list of projects within your workspace (the workspaceId query param used elsewhere in this API is a project ID, see the note under List contacts).
#Other endpoints
These cover more specialized inbox workflows and are not detailed here:
| Endpoint | Description |
|---|---|
POST /inbox/compose | Start a new (non-reply) conversation over email or LinkedIn. |
POST /inbox/sync | Trigger an IMAP sync for one account (?accountId=) or all of your workspace's accounts. Returns { "result": ... } for one account or { "results": [...] } for all. |
POST /inbox/contacts/{contactId}/draft-reply | Generate an AI draft reply for a contact's latest inbound email and autosave it. |
GET/POST /inbox/drafts, PATCH/DELETE /inbox/drafts/{id} | Email drafts saved against one mailbox. GET requires ?accountId= and lists every draft on that mailbox, autosaved ones included, so there is no cross-mailbox listing. POST returns 201. |
POST /inbox/mark-all-read | Mark unread inbound messages as read. Pass contactIds (up to 500) to mark just those conversations, or omit it to mark the whole workspace inbox. Returns { "marked": n }. |
GET /inbox/warmup-jobs | Warmup job activity feed for the inbox view. |
PATCH/DELETE /inbox/messages/{id} | Update flags on (read/starred/archived) or the category of, or delete, a single message. |
GET /inbox/messages/{id}/attachments/{part} | Download a specific email attachment part. |