Less boo. More booked. Halloween offer: 30 days free on any paid plan. Claim 30 days freeEvery new account gets one free mailbox for warmup and replies. No domain to buy. See how it works

Skip to content

Inbox

API reference11 min read7 sections

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.

bash
curl https://app.warmerly.com/api/v1/inbox/contacts \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API reference

#List contacts

text
GET /inbox/contacts

Returns 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 paramRequiredDescription
workspaceIdNoFilter to contacts belonging to a single project. Despite the name, this is a project ID (the inbox filter bar labels projects as "workspaces").
accountIdNoFilter 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.
qNoSearch contact name, email, phone and company (substring match) and message content (subject, body, sender; each term matches by prefix).
needsReplyNotrue, only conversations whose newest message is inbound and is either classified as a lead or not classified yet.
archivedNotrue, only conversations where every message is archived.
unreadNotrue, only conversations with at least one unread inbound message.
categoryNolead, not_relevant, or spam. Any other value is ignored.
channelNoemail, linkedin, or whatsapp. Any other value is ignored.
limitNoPage size, 1-200 (default 50). Values outside the range are clamped.
offsetNoPage 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.

bash
curl "https://app.warmerly.com/api/v1/inbox/contacts?accountId=8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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
}
FieldTypeDescription
idstring (UUID)Contact ID, use this for the timeline and reply endpoints.
namestringDisplay name, falling back to email or phone if unset.
companystring \| nullCompany name, when known from an associated campaign lead.
campaignNamestring \| nullName of that campaign lead's campaign, when there is one.
emailstring \| nullContact's email address.
phonestring \| nullContact's phone number, when known.
linkedinProfileIdstring \| nullLinkedIn profile identifier, when known.
linkedinProfileUrlstring \| nullLinkedIn profile URL, when known.
channelsarrayChannels (email, linkedin) this contact has any message on.
lastActivityAtstringISO 8601 timestamp of the most recent message. A failed outbound send does not count.
hasUnreadbooleanTrue if there's at least one unread inbound message.
lastPreviewstring \| nullSnippet of the most recent message.
category"lead" \| "not_relevant" \| "spam" \| nullAI/rule-assigned triage category.
categoryReasonstring \| nullShort explanation for the assigned category.
latestInboundMessageIdstring \| nullID of the newest inbound message.
latestDirection"inbound" \| "outbound" \| nullDirection of the newest message (failed outbound sends ignored).
latestChannel"email" \| "linkedin" \| "whatsapp" \| nullChannel of the newest message.
latestInboundChannel"email" \| "linkedin" \| "whatsapp" \| nullChannel of the newest inbound message.
isArchivedbooleanTrue if every message in the conversation is archived.
API reference

#Get a contact's timeline

text
GET /inbox/contacts/{contactId}/timeline

Returns 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.

bash
curl https://app.warmerly.com/api/v1/inbox/contacts/c3a1f6e2-9c4d-4e2a-8b5f-6a2c1d9e0f7b/timeline \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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"
  }
}
FieldTypeDescription
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[].contentstringPlain-text body (falls back to snippet if the full body wasn't stored).
items[].hasAttachmentsbooleanWhether the message has attachments.
items[].attachmentsMetaarray \| nullAttachment details (filename, contentType, size, part), usable with the attachment download endpoint.
items[].isWarmupbooleanTrue for Warmerly warmup traffic.
items[].sentBy"autopilot" \| null"autopilot" when LinkedIn AI Autopilot sent the message.
draftobject \| nullAutosaved email draft for the latest inbound email thread, if one exists (subject, bodyText).
latestSubjectstring \| nullSubject of the most recent message that has one.
replyAccountIdstring \| nullThe 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:

json
{ "error": { "code": "not_found", "message": "not_found", "details": null } }
API reference

#Reply to a contact

text
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:

FieldTypeRequiredDescription
channel"email" \| "linkedin"YesChannel to send on.
bodystringYesMessage text, at least 1 character.
subjectstringEmail onlyRequired when channel is "email". A Re: prefix is added if missing.
sourceAccountIdstringYesID of the connected account to send from (email account or LinkedIn account, matching channel). Must belong to your workspace, otherwise 400 invalid_source_account.
bash
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"
      }'
json
{
  "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 email on file and subject is required. If the contact already has an email thread in your workspace, the reply goes out from the mailbox that thread started on, even if sourceAccountId names another of your mailboxes. A send failure returns 500 with the SMTP error message, but the failed message is still recorded in the timeline with status: "failed".
  • LinkedIn sends synchronously through your connected LinkedIn account; the contact must have a linkedinProfileId on file. A send failure returns 500 and is recorded as failed, as for email. Some account types cannot send yet and return 400 (for example native_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:

json
{ "error": { "code": "bad_request", "message": "contact_has_no_email", "details": null } }
API reference

#List threads

text
GET /inbox/threads

Returns 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 paramRequiredDescription
accountIdNoFilter to a single email account. An account that is not in your workspace returns 404 not_found.
folderNoIMAP folder to filter by. Defaults to INBOX. Ignored unless view is inbox.
viewNoOne of inbox (default), archived, starred, sent. Any other value applies no view filter.
qNoFull-text search across subject, sender, and body (Postgres FTS with an ILIKE fallback).
limitNoMax threads to return, 1 to 100. Defaults to 50.
bash
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"
json
{
  "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?"
    }
  ]
}
API reference

#Get a thread

text
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 paramRequiredDescription
accountIdYesThe email account the thread is on. Missing: 400 bad_request. An account that is not in your workspace: 404 not_found.
bash
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"
json
{
  "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.

API reference

#Filter options

text
GET /inbox/filter-options

Returns 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.

bash
curl https://app.warmerly.com/api/v1/inbox/filter-options \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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).

API reference

#Other endpoints

These cover more specialized inbox workflows and are not detailed here:

EndpointDescription
POST /inbox/composeStart a new (non-reply) conversation over email or LinkedIn.
POST /inbox/syncTrigger 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-replyGenerate 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-readMark 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-jobsWarmup 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.
Was this page helpful?