# Inbox

> API reference for the Warmerly Inbox: list contacts, pull a unified email/LinkedIn conversation timeline, and send replies across channels.

Source: https://warmerly.com/docs/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.

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

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

```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
}
```

| 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

```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"
  }
}
```

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

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

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

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

```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 } }
```

## 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 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`. |

```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?"
    }
  ]
}
```

## 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 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`. |

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

## 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](https://warmerly.com/docs/inbox#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. |

## Each part of this reference has its own page (6)

- [Get a contact's timeline](https://warmerly.com/docs/inbox/get-a-contact-s-timeline)
- [Reply to a contact](https://warmerly.com/docs/inbox/reply-to-a-contact)
- [List threads](https://warmerly.com/docs/inbox/list-threads)
- [Get a thread](https://warmerly.com/docs/inbox/get-a-thread)
- [Filter options](https://warmerly.com/docs/inbox/filter-options)
- [Other endpoints](https://warmerly.com/docs/inbox/other-endpoints)

## More in API reference

- [Leads](https://warmerly.com/docs/leads): API reference for the Warmerly lead database: search millions of crawled companies, export matching rows to…
- [Suppression](https://warmerly.com/docs/suppression): API reference for the Warmerly suppression list: add addresses that must never be emailed in any campaign,…
- [Webhooks](https://warmerly.com/docs/webhooks): API reference for Warmerly webhook subscriptions: get HTTP-signed notifications for mailbox health events…
