Inbox
The Inbox API exposes Warmerly's unified inbox — every email, LinkedIn DM, and WhatsApp message 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 your active workspace (the same one
selected in the dashboard), which can span multiple projects at once. If you're a
member of more than one workspace, requests use whichever workspace is currently
active on your account.
curl https://app.warmerly.com/api/v1/inbox/contacts \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
List contacts
GET /inbox/contacts
Returns every inbox contact in your active workspace — the people/companies you've exchanged messages with across email, LinkedIn, and WhatsApp — newest activity first.
| 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 (email account today). |
q | No | Search contact name/email/phone/company and message content (subject, body, sender). Terms match by prefix. |
needsReply | No | true — only conversations classified as a lead whose newest message is inbound. |
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. |
channel | No | email, linkedin, or whatsapp. |
limit | No | Page size, 1–200 (default 50). |
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",
"avatarUrl": null,
"email": "jamie@northwindtraders.com",
"phone": "+15551234567",
"hasWhatsapp": true,
"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"
}
],
"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. |
email | string | null | Contact's email address. |
phone | string | null | Contact's phone number, when known. |
hasWhatsapp | boolean | True if the contact has a phone number or a raw WhatsApp JID on file. Use this (not phone) to decide whether a WhatsApp reply is possible. |
linkedinProfileUrl | string | null | LinkedIn profile URL, when known. |
channels | array | Channels (email, linkedin, whatsapp) this contact has any message on. |
lastActivityAt | string | ISO 8601 timestamp of the most recent message. |
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. |
Get a contact's timeline
GET /inbox/contacts/{contactId}/timeline
Returns the full conversation with a contact across all channels, in chronological order, plus any autosaved email draft in progress for the latest inbound thread.
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
},
{
"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
}
],
"draft": null,
"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" | "whatsapp" | Channel the message was sent/received on. |
items[].direction | "inbound" | "outbound" | Who sent it. |
items[].status | "queued" | "sent" | "failed" | Delivery status (WhatsApp sends start queued until the worker delivers them). |
items[].content | string | Plain-text body (falls back to snippet if the full body wasn't stored). |
draft | object | null | Autosaved email draft for the latest inbound email thread, if one exists (subject, bodyText). |
If contactId doesn't exist, or belongs to a contact outside your active workspace,
this returns 404:
{ "error": { "code": "not_found", "message": "not_found", "details": null } }
Reply to a contact
POST /inbox/contacts/{contactId}/reply
Sends 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" | "whatsapp" | Yes | Channel to send on. |
body | string | Yes | Message text. |
subject | string | Email only | Required when channel is "email". |
sourceAccountId | string | Yes | ID of the connected account to send from (email account, LinkedIn account, or WhatsApp account, matching channel). |
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"
}
}
Notes on behavior by channel:
- Email sends synchronously over SMTP; the contact must have an
emailon file andsubjectis required. A send failure returns500with the SMTP error message, but the failed message is still recorded in the timeline withstatus: "failed". - LinkedIn sends synchronously via Unipile; the contact must have a
linkedinProfileIdon file. - WhatsApp is sent asynchronously — the message is inserted with
status: "queued"and delivered by a background worker shortly after. The contact must have a phone number or WhatsApp JID on file.
Missing channel-specific data returns 400, e.g.:
{ "error": { "code": "bad_request", "message": "contact_has_no_email", "details": null } }
List threads
GET /inbox/threads
Returns email threads (grouped by threadKey + accountId) with aggregate counts,
for building a mailbox-style view. 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. |
folder | No | IMAP folder to filter by. Defaults to INBOX. Ignored unless view is inbox. |
view | No | One of inbox (default), archived, starred, sent. |
q | No | Full-text search across subject, sender, and body (Postgres FTS with an ILIKE fallback). |
limit | No | Max threads to return, 1–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 every raw message in a single email thread, chronological order. {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 | No | Narrow to messages on this account (recommended — thread keys aren't guaranteed unique across accounts). |
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 (unfiltered by workspace/project
ownership) rather than the trimmed shape used by the timeline endpoint — scope your
query with accountId to keep results to accounts you control.
Filter options
GET /inbox/filter-options
Returns the projects and email accounts available to filter the inbox 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 active 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, LinkedIn, or WhatsApp. |
POST /inbox/sync | Trigger an IMAP sync for one account (?accountId=) or all connected accounts. |
POST /inbox/contacts/{contactId}/draft-reply | Generate an AI draft reply for a contact's latest inbound email and autosave it. |
GET/POST /inbox/templates, PATCH/DELETE /inbox/templates/{id} | Reusable reply templates. |
GET/POST /inbox/drafts, PATCH/DELETE /inbox/drafts/{id} | Manually saved (non-autosaved) email drafts. |
GET /inbox/warmup-jobs | Warmup job activity feed for the inbox view. |
PATCH/DELETE /inbox/messages/{id} | Update flags on (read/starred/archived), or delete, a single message. |
GET /inbox/messages/{id}/attachments/{part} | Download a specific email attachment part. |
