API Documentation & Integrations

Complete REST API documentation for WarmySender. Build custom integrations with our cold email, email warmup, campaign, and LinkedIn automation APIs.

OpenAPI specification

The full machine-readable contract is published as an OpenAPI 3.1 document: Download the OpenAPI 3.1 spec. Import it into Postman, Insomnia, Swagger UI, or Redoc to generate a client, explore every endpoint interactively, or scaffold typed SDKs. The reference below is generated from the same source, so it always matches the spec and production.

Authentication

All API requests require a Bearer token in the Authorization header. Generate API keys from Settings > API Keys in your WarmySender dashboard. Keys are scoped to your workspace and support granular permissions.

Authorization: Bearer your-api-key-here

Base URL

All API endpoints are available at:

https://warmysender.com/api/v1

Rate Limiting

API requests are limited to 60 requests per minute per workspace. Rate limit headers are included in all responses:

Scopes

API keys are scoped to your workspace with granular permissions. Each endpoint lists the scope it needs.

API reference

Every endpoint below is generated from the same source as our OpenAPI 3.1 specification, so the request bodies, response shapes, parameters, and scopes always match production. Base URL for all paths: https://warmysender.com/api/v1.

Identity

Get the authenticated API key + workspace

GET /api/v1/me

Returns the API key and workspace that the Bearer token belongs to, including the key's granted scopes, plus the account's current standing. Useful for verifying a key, discovering its permissions, and checking whether the account can send before you attempt any write. The billing block is present only when the account is suspended over an outstanding invoice. This is the one endpoint whose response is NOT wrapped in the standard data envelope.

Response — 200
{
  "apiKey": {
    "id": "key_8f1c2a",
    "name": "Production integration",
    "scopes": [
      "mailboxes:read",
      "mailboxes:write",
      "warmup:read"
    ]
  },
  "workspace": {
    "id": "ws_4b9d10"
  },
  "account_status": "suspended",
  "account_status_label": "Suspended — an invoice is outstanding",
  "account_suspended": true,
  "can_send": false,
  "billing": {
    "reason": "An invoice on the account has not been settled yet.",
    "what_to_do": "Settle the outstanding invoice and sending resumes automatically.",
    "amount_outstanding": "$32.99",
    "amount_outstanding_cents": 3299,
    "currency": "USD",
    "invoice_number": "B1C2D3-0004",
    "pay_url": "https://invoice.stripe.com/i/...",
    "since": "2026-07-29T12:52:00.000Z"
  }
}
Errors

Workspace

Get the cold email footer settings

GET /api/v1/workspace/settings

Returns the postal address printed in the footer of your cold email, how people opt out of it, and the exact sentence the footer will show. "link": The footer carries an Unsubscribe link — on your own verified tracking domain when you have one, otherwise on a shared WarmySender domain. "reply": The footer reads “Reply "unsubscribe" to opt out” and points back at the sending mailbox — no third-party link at all. Either way, a reply asking to unsubscribe is honored automatically and that person is never contacted again. Writing your own sentence? Put the word in quotes — like Reply “stop” — and a reply of just that word is honored too. opt_out_text is your own wording, or null when you have not set any; opt_out_text_effective is what the footer actually shows. timezone is your workspace timezone — every new campaign starts with it.

Response — 200
{
  "data": {
    "company_address": "120 Market Street, Suite 400, San Francisco, CA 94105",
    "opt_out_style": "link",
    "opt_out_text": null,
    "opt_out_text_effective": "Unsubscribe",
    "timezone": "Europe/Paris"
  }
}
Errors

Update the cold email footer settings

PATCH /api/v1/workspace/settings

Change the postal address printed in the footer of your cold email, the way people opt out, the wording of the opt-out sentence, your workspace timezone, or any combination. Send at least one field; anything you leave out is unchanged. This sends nothing and changes no sending limit.

Request body
Request example
{
  "company_address": "120 Market Street, Suite 400, San Francisco, CA 94105",
  "opt_out_style": "reply",
  "opt_out_text": "Reply \"stop\" and we will not write again"
}
Response — 200
{
  "data": {
    "company_address": "120 Market Street, Suite 400, San Francisco, CA 94105",
    "opt_out_style": "reply",
    "opt_out_text": "Reply \"stop\" and we will not write again",
    "opt_out_text_effective": "Reply \"stop\" and we will not write again",
    "timezone": "Europe/Paris"
  }
}
Errors

Mailboxes

List mailboxes

GET /api/v1/mailboxes

Returns connected mailboxes, newest first, with cursor pagination. Soft-deleted mailboxes are excluded unless you filter by status=deleted. Each mailbox carries `campaigns` — the campaigns it sends for (`id`, `name`, `status`, `channel`; running first, up to 50) — and the exact `campaign_count`.

Parameters
Response — 200
{
  "data": [
    {
      "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
      "external_id": "crm-contact-8841",
      "email_address": "[email protected]",
      "display_name": "Jordan Lee",
      "status": "connected",
      "health_status": "ok",
      "health_reasons": null,
      "read_channel_state": "ok",
      "read_channel_verified": true,
      "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
      "read_channel_state_label": "Inbox readable",
      "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
      "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_progress": 62,
      "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
      "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
      "incoming_warmup_state": "receiving",
      "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
      "incoming_warmup_paused": false,
      "incoming_warmup_pause_needs_review": false,
      "incoming_warmup_resumes_at": null,
      "domain_blocked_for_incoming_warmup": false,
      "last_received_at": "2026-06-10T07:58:00.000Z",
      "days_since_last_received": 0,
      "domain_on_blocklist": false,
      "domain_blocklist_lists": null,
      "domain_blocklist_detail": null,
      "domain_blocklist_help_url": null,
      "sending_enabled": true,
      "daily_send_limit": 50,
      "last_tested_at": "2026-06-10T08:31:00.000Z",
      "last_test_error": null,
      "signature": null,
      "created_at": "2026-06-06T10:43:01.357Z",
      "updated_at": "2026-06-10T08:31:05.220Z",
      "campaigns": [
        {
          "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
          "name": "Q3 Outbound — Founders",
          "status": "running",
          "channel": "email"
        }
      ],
      "campaign_count": 1
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}

Get a mailbox

GET /api/v1/mailboxes/{id}

Fetch a single mailbox by id. Poll this after create/restore until status becomes "connected". The response also carries the incoming-warmup fields (`incoming_warmup_state` and friends) describing whether warmup is actually arriving at this mailbox, and the read-channel fields (`read_channel_state` and friends) describing whether we can open the mailbox and read it at all. `status` and `health_status` cover the sending side only — a mailbox can be "connected" and "ok" while its inbox cannot be opened, so check `read_channel_state` before concluding a mailbox is fully working. `campaigns` / `campaign_count` list the campaigns this mailbox sends for.

Parameters
Response — 200
{
  "data": {
    "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "external_id": "crm-contact-8841",
    "email_address": "[email protected]",
    "display_name": "Jordan Lee",
    "status": "connected",
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "sending_enabled": true,
    "daily_send_limit": 50,
    "last_tested_at": "2026-06-10T08:31:00.000Z",
    "last_test_error": null,
    "signature": null,
    "created_at": "2026-06-06T10:43:01.357Z",
    "updated_at": "2026-06-10T08:31:05.220Z",
    "campaigns": [
      {
        "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "name": "Q3 Outbound — Founders",
        "status": "running",
        "channel": "email"
      }
    ],
    "campaign_count": 1
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Create a mailbox

POST /api/v1/mailboxes

Adds a mailbox and runs an asynchronous connection test (status moves created → testing → connected or error). If the email matches a previously soft-deleted mailbox, that mailbox is restored instead and the response is 200 with its preserved warmup history (otherwise 201).

Request body
Request example
{
  "email_address": "[email protected]",
  "smtp_host": "smtp.gmail.com",
  "smtp_port": 587,
  "smtp_username": "[email protected]",
  "smtp_password": "app-password",
  "imap_host": "imap.gmail.com",
  "imap_port": 993,
  "imap_username": "[email protected]",
  "imap_password": "app-password",
  "external_id": "crm-contact-8841",
  "auto_enable_warmup": true
}
Response — 201
{
  "data": {
    "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "external_id": "crm-contact-8841",
    "email_address": "[email protected]",
    "display_name": "Jordan Lee",
    "status": "testing",
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 0,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "sending_enabled": true,
    "daily_send_limit": 50,
    "last_tested_at": null,
    "last_test_error": null,
    "signature": null,
    "created_at": "2026-06-06T10:43:01.357Z",
    "updated_at": "2026-06-10T08:31:05.220Z"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6",
    "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
  }
}
Errors

Test credentials (no save)

POST /api/v1/mailboxes/test

Synchronously tests SMTP + IMAP credentials WITHOUT creating a mailbox. Rate limited.

Request body
Request example
{
  "email_address": "[email protected]",
  "smtp_host": "smtp.gmail.com",
  "smtp_port": 587,
  "smtp_username": "[email protected]",
  "smtp_password": "app-password",
  "imap_host": "imap.gmail.com",
  "imap_port": 993,
  "imap_username": "[email protected]",
  "imap_password": "app-password"
}
Response — 200
{
  "data": {
    "smtp_ok": true,
    "imap_ok": true
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Update a mailbox

PATCH /api/v1/mailboxes/{id}

Partial update — only the fields you send change. Changing SMTP/IMAP credentials re-runs the connection test. Set external_id or signature to null to clear them.

Parameters
Request body
Request example
{
  "display_name": "Jordan Lee",
  "daily_send_limit": 80
}
Response — 200
{
  "data": {
    "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "external_id": "crm-contact-8841",
    "email_address": "[email protected]",
    "display_name": "Jordan Lee",
    "status": "connected",
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "sending_enabled": true,
    "daily_send_limit": 80,
    "last_tested_at": "2026-06-10T08:31:00.000Z",
    "last_test_error": null,
    "signature": null,
    "created_at": "2026-06-06T10:43:01.357Z",
    "updated_at": "2026-06-10T08:31:05.220Z"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Delete (soft) a mailbox

DELETE /api/v1/mailboxes/{id}

Soft-deletes the mailbox: it stops sending and warmup immediately but its history is preserved so it can be restored. Returns 200 with the mailbox object (status "deleted") — not 204.

Parameters
Response — 200
{
  "data": {
    "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "external_id": "crm-contact-8841",
    "email_address": "[email protected]",
    "display_name": "Jordan Lee",
    "status": "deleted",
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "warmup_enabled": false,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "sending_enabled": true,
    "daily_send_limit": 50,
    "last_tested_at": "2026-06-10T08:31:00.000Z",
    "last_test_error": null,
    "signature": null,
    "created_at": "2026-06-06T10:43:01.357Z",
    "updated_at": "2026-06-10T08:31:05.220Z"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Restore a deleted mailbox

POST /api/v1/mailboxes/{id}/restore

Brings a soft-deleted mailbox back with its warmup history intact and re-runs the connection test (status returns to "created" → "connected"). Warmup starts disabled; enable it once connected. Blocked if an active mailbox already uses the same email.

Parameters
Response — 200
{
  "data": {
    "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "external_id": "crm-contact-8841",
    "email_address": "[email protected]",
    "display_name": "Jordan Lee",
    "status": "created",
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "warmup_enabled": false,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "sending_enabled": true,
    "daily_send_limit": 50,
    "last_tested_at": "2026-06-10T08:31:00.000Z",
    "last_test_error": null,
    "signature": null,
    "created_at": "2026-06-06T10:43:01.357Z",
    "updated_at": "2026-06-10T08:31:05.220Z"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Re-test a mailbox connection

POST /api/v1/mailboxes/{id}/test

Re-runs the connection test against the stored credentials and updates the mailbox status. Rate limited. The mailbox status reflects the SENDING side only — a problem reading the inbox is reported in test_result.read_channel and never stops the mailbox from sending.

Parameters
Response — 200
{
  "data": {
    "mailbox": {
      "id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
      "external_id": "crm-contact-8841",
      "email_address": "[email protected]",
      "display_name": "Jordan Lee",
      "status": "connected",
      "health_status": "ok",
      "health_reasons": null,
      "read_channel_state": "ok",
      "read_channel_verified": true,
      "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
      "read_channel_state_label": "Inbox readable",
      "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
      "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_progress": 62,
      "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
      "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
      "incoming_warmup_state": "receiving",
      "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
      "incoming_warmup_paused": false,
      "incoming_warmup_pause_needs_review": false,
      "incoming_warmup_resumes_at": null,
      "domain_blocked_for_incoming_warmup": false,
      "last_received_at": "2026-06-10T07:58:00.000Z",
      "days_since_last_received": 0,
      "domain_on_blocklist": false,
      "domain_blocklist_lists": null,
      "domain_blocklist_detail": null,
      "domain_blocklist_help_url": null,
      "sending_enabled": true,
      "daily_send_limit": 50,
      "last_tested_at": "2026-06-10T08:31:00.000Z",
      "last_test_error": null,
      "signature": null,
      "created_at": "2026-06-06T10:43:01.357Z",
      "updated_at": "2026-06-10T08:31:05.220Z"
    },
    "test_result": {
      "smtp_ok": true,
      "imap_ok": true,
      "read_channel": {
        "state": "ok",
        "verified": true,
        "code": null
      }
    }
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Warmup

List warmup stats

GET /api/v1/warmup/stats

Per-mailbox warmup health in BOTH directions: sending-side progress, today's volume, placement and reputation, plus the incoming side — whether warmup is actually arriving at the mailbox (`incoming_warmup_state`), when it last did, and whether it is paused and why. `inbox_rate` measures the SENDING side only; `inbox_rate_measures` spells that out so the two are never conflated. `inbox_rate_scope` says where it is measured: warmup emails between mailboxes in the warmup network, never your own contacts. Cursor paginated.

Parameters
Response — 200
{
  "data": [
    {
      "mailbox_id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
      "email_address": "[email protected]",
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_mode": "new_mailbox",
      "warmup_mode_reason": "Strategy set by you.",
      "warmup_progress": 62,
      "warmup_target_daily_volume": 50,
      "language": "en",
      "effective_daily_limit": 50,
      "effective_limit_reason": null,
      "spam_hold": null,
      "sent_today": 18,
      "received_today": 17,
      "reputation_score": 96,
      "inbox_rate": 98,
      "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
      "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
      "inbox_rate_sample": 240,
      "inbox_rate_reliable": true,
      "inbox_rate_sample_measures": "how many of this mailbox's sent warmup emails we confirmed the placement of — at 0 the rate above is a placeholder default, not a result",
      "spam_rate": 1,
      "last_sent_at": "2026-06-10T09:12:00.000Z",
      "incoming_warmup_state": "receiving",
      "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
      "incoming_warmup_paused": false,
      "incoming_warmup_pause_needs_review": false,
      "incoming_warmup_resumes_at": null,
      "domain_blocked_for_incoming_warmup": false,
      "last_received_at": "2026-06-10T07:58:00.000Z",
      "days_since_last_received": 0,
      "domain_on_blocklist": false,
      "domain_blocklist_lists": null,
      "domain_blocklist_detail": null,
      "domain_blocklist_help_url": null,
      "health_status": "ok",
      "health_reasons": null,
      "read_channel_state": "ok",
      "read_channel_verified": true,
      "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
      "read_channel_state_label": "Inbox readable",
      "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
      "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}

Get warmup stats for one mailbox

GET /api/v1/warmup/stats/{mailboxId}

Warmup health for a single mailbox, in both directions: the sending side (volume, placement, reputation) and the incoming side (`incoming_warmup_state`, `last_received_at`, and a plain-language `incoming_warmup_state_detail` explaining any pause). Also returns `placement_by_provider` — where this mailbox's warmup emails landed over the last 14 days, split by the receiving provider (Google, Microsoft, other providers) — and `spam_hold`, a plain sentence explaining a reduced daily volume while the spam check holds it (null otherwise).

Parameters
Response — 200
{
  "data": {
    "mailbox_id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "email_address": "[email protected]",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_mode": "new_mailbox",
    "warmup_mode_reason": "Strategy set by you.",
    "warmup_progress": 62,
    "warmup_target_daily_volume": 50,
    "language": "en",
    "effective_daily_limit": 50,
    "effective_limit_reason": null,
    "spam_hold": null,
    "sent_today": 18,
    "received_today": 17,
    "reputation_score": 96,
    "inbox_rate": 98,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "inbox_rate_sample": 240,
    "inbox_rate_reliable": true,
    "inbox_rate_sample_measures": "how many of this mailbox's sent warmup emails we confirmed the placement of — at 0 the rate above is a placeholder default, not a result",
    "spam_rate": 1,
    "last_sent_at": "2026-06-10T09:12:00.000Z",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree",
    "placement_by_provider": {
      "window_days": 14,
      "window_start": "2026-09-23T00:00:00.000Z",
      "providers": [
        {
          "provider": "google",
          "label": "Google",
          "sent": 11,
          "inbox": 10,
          "spam": 1,
          "other_folder": 0,
          "bounced": 0,
          "unconfirmed": 0
        },
        {
          "provider": "microsoft",
          "label": "Microsoft",
          "sent": 6,
          "inbox": 6,
          "spam": 0,
          "other_folder": 0,
          "bounced": 0,
          "unconfirmed": 0
        },
        {
          "provider": "other",
          "label": "Other providers",
          "sent": 9,
          "inbox": 8,
          "spam": 0,
          "other_folder": 0,
          "bounced": 0,
          "unconfirmed": 1
        }
      ],
      "totals": {
        "sent": 26,
        "inbox": 24,
        "spam": 1,
        "other_folder": 0,
        "bounced": 0,
        "unconfirmed": 1
      },
      "spam_not_split_by_provider": 0,
      "measures": "where the warmup emails this mailbox SENT over the last 14 days landed, grouped by the provider of the mailbox that received each one"
    }
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Update warmup settings

PATCH /api/v1/warmup/settings/{mailboxId}

Enable/disable warmup, switch strategy, or set the target daily volume for one mailbox. Enabling requires the mailbox status to be "connected". A strategy change updates both `warmup_type` and `warmup_mode` (the field the ramp reads). If a faster strategy cannot take effect yet because recent deliverability is poor, the response carries a plain-language `meta.warning` — the change is recorded, but the safer pace stays in force until placement recovers.

Parameters
Request body
Request example
{
  "warmup_enabled": true,
  "warmup_type": "normal",
  "warmup_target_daily_volume": 50
}
Response — 200
{
  "data": {
    "mailbox_id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "email_address": "[email protected]",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_mode": "new_mailbox",
    "warmup_mode_reason": "Strategy set by you.",
    "warmup_progress": 62,
    "warmup_target_daily_volume": 50,
    "language": "en",
    "effective_daily_limit": 50,
    "effective_limit_reason": null,
    "spam_hold": null,
    "sent_today": 18,
    "received_today": 17,
    "reputation_score": 96,
    "inbox_rate": 98,
    "inbox_rate_measures": "warmup this mailbox SENT that landed in recipients' inboxes — not what arrives here",
    "inbox_rate_scope": "Measured only on warmup emails exchanged with mailboxes in the WarmySender warmup network (warmup never emails your own contacts); it shows how providers currently treat mail from this mailbox, not whether a specific email to a contact arrived, which can't be seen from here.",
    "inbox_rate_sample": 240,
    "inbox_rate_reliable": true,
    "inbox_rate_sample_measures": "how many of this mailbox's sent warmup emails we confirmed the placement of — at 0 the rate above is a placeholder default, not a result",
    "spam_rate": 1,
    "last_sent_at": "2026-06-10T09:12:00.000Z",
    "incoming_warmup_state": "receiving",
    "incoming_warmup_state_detail": "Warmup is arriving at this mailbox normally.",
    "incoming_warmup_paused": false,
    "incoming_warmup_pause_needs_review": false,
    "incoming_warmup_resumes_at": null,
    "domain_blocked_for_incoming_warmup": false,
    "last_received_at": "2026-06-10T07:58:00.000Z",
    "days_since_last_received": 0,
    "domain_on_blocklist": false,
    "domain_blocklist_lists": null,
    "domain_blocklist_detail": null,
    "domain_blocklist_help_url": null,
    "health_status": "ok",
    "health_reasons": null,
    "read_channel_state": "ok",
    "read_channel_verified": true,
    "read_channel_verified_at": "2026-06-10T07:58:00.000Z",
    "read_channel_state_label": "Inbox readable",
    "read_channel_state_detail": "We have opened this mailbox and read what arrived, so replies and warmup confirmations can be seen.",
    "read_channel_measures": "whether we can open this mailbox and read what arrives — a separate check from whether it can send, and the two can disagree"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Bulk-update warmup settings (async)

POST /api/v1/warmup/bulk-update

Applies the same warmup changes to many mailboxes at once. Returns 202 with a job id immediately — poll GET /jobs/{id} for progress. Mailboxes that can't be enabled (not connected) are recorded as per-item errors on the job. A strategy change updates both `warmup_type` and `warmup_mode` (the field the ramp reads) on every mailbox in the batch.

Request body
Request example
{
  "mailbox_ids": [
    "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
  ],
  "updates": {
    "warmup_enabled": true,
    "warmup_type": "normal"
  }
}
Response — 202
{
  "data": {
    "job_id": "job_7d2f1a",
    "status": "processing",
    "total_items": 2
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6",
    "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
  }
}
Errors

Jobs

Get job status

GET /api/v1/jobs/{id}

Status and progress of an asynchronous job (e.g. a warmup bulk-update). Per-item failures are listed in errors.

Parameters
Response — 200
{
  "data": {
    "id": "job_7d2f1a",
    "type": "bulk_warmup_update",
    "status": "completed",
    "total_items": 2,
    "processed_items": 2,
    "failed_items": 0,
    "errors": [],
    "created_at": "2026-06-10T12:00:00.000Z",
    "completed_at": "2026-06-10T12:00:04.000Z"
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Prospects

List prospects

GET /api/v1/prospects

Returns prospects in your workspace, newest first, with cursor pagination. Pass listId to return only the prospects in one list — the way to pull everyone in a list from your own server or script. Get list ids from GET /lists. Pass field and fieldValue to keep only the prospects whose job title, company, country, city or one of your own CSV columns contains a value — the same "Filter by field" as the Prospects page.

Parameters
Response — 200
{
  "data": [
    {
      "id": "f3c4a2d1-9b7e-4c2a-8f10-2b6d5e8a1c44",
      "email": "[email protected]",
      "firstName": "Jane",
      "lastName": "Doe",
      "company": "Acme Inc",
      "role": "CTO",
      "phone": null,
      "globalStatus": "active",
      "linkedinUrl": "https://www.linkedin.com/in/janedoe",
      "instagramUsername": "janedoe",
      "whatsappPhone": "491712345678",
      "customFields": {
        "plan": "enterprise"
      },
      "createdAt": "2026-06-09T14:02:00.000Z",
      "updatedAt": "2026-06-09T14:02:00.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

List prospect lists

GET /api/v1/lists

Returns the lists in your workspace, newest first, with cursor pagination. Each list includes its id, name and how many prospects it holds. Use a list id with GET /prospects?listId=<id> to pull the prospects in that list.

Parameters
Response — 200
{
  "data": [
    {
      "id": "d4e5f6a7-b8c9-4012-8345-6789abcdef01",
      "name": "EU ops leads",
      "description": "Imported from the June trade show",
      "prospectCount": 1240,
      "createdAt": "2026-06-01T09:00:00.000Z",
      "updatedAt": "2026-06-12T16:30:00.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}

Delete a prospect list

DELETE /api/v1/lists/{id}

Deletes one of your lists, exactly like deleting it in the app. Prospects that are in this list and in no other list are permanently deleted with it, together with their campaign history. Prospects that are also in another list are kept. If any prospect that would be deleted is still active in a campaign, nothing is deleted and the response names the campaigns to pause first.

Parameters
Response — 200
{
  "data": {
    "deleted": true,
    "id": "d4e5f6a7-b8c9-4012-8345-6789abcdef01",
    "contacts_deleted": 12
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Create a prospect

POST /api/v1/prospects

Creates a prospect. Provide at least one of email, linkedinUrl, instagramUsername, or whatsappPhone — a prospect with no email address is still created and can be enrolled in LinkedIn, Instagram or WhatsApp campaigns. If one with the same email already exists it is merged and returned with alreadyExisted=true (HTTP 200); a brand-new prospect returns 201. Pass listId to drop the prospect into one of your lists; if a running campaign is linked to that list the prospect is auto-enrolled (continuous enrollment) — ideal for a website signup or popup form. Pass enrollInCampaignId instead to enroll into one specific campaign in the same call.

Request body
Request example
{
  "email": "[email protected]",
  "firstName": "Sam",
  "listId": "list_9f3a2b",
  "enroll": true
}
Response — 201
{
  "data": {
    "id": "f3c4a2d1-9b7e-4c2a-8f10-2b6d5e8a1c44",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "company": "Acme Inc",
    "role": "CTO",
    "phone": null,
    "globalStatus": "active",
    "linkedinUrl": "https://www.linkedin.com/in/janedoe",
    "instagramUsername": "janedoe",
    "whatsappPhone": "491712345678",
    "customFields": {
      "plan": "enterprise"
    },
    "createdAt": "2026-06-09T14:02:00.000Z",
    "updatedAt": "2026-06-09T14:02:00.000Z"
  },
  "alreadyExisted": false,
  "listId": "list_9f3a2b",
  "list": {
    "attached": true,
    "enrolledCampaigns": 1
  }
}
Errors

Update a prospect

PATCH /api/v1/prospects/{id}

Partial update of a prospect. Accepts every field the in-app contact editor exposes except email (change the address in the app) and status (bounced / unsubscribed / do-not-contact cannot be lifted here). website, country, city, industry, tags and custom fields are included. linkedinUrl, instagramUsername and whatsappPhone may be set or cleared (null). A field name that is not on this list is refused and named in the error.

Parameters
Request body
Request example
{
  "role": "VP Engineering",
  "industry": "SaaS",
  "tags": [
    "vip"
  ],
  "customFields": {
    "plan": "enterprise"
  }
}
Response — 200
{
  "data": {
    "id": "f3c4a2d1-9b7e-4c2a-8f10-2b6d5e8a1c44",
    "email": "[email protected]",
    "firstName": "Jane",
    "lastName": "Doe",
    "company": "Acme Inc",
    "role": "VP Engineering",
    "phone": null,
    "globalStatus": "active",
    "linkedinUrl": "https://www.linkedin.com/in/janedoe",
    "instagramUsername": "janedoe",
    "whatsappPhone": "491712345678",
    "customFields": {
      "plan": "enterprise"
    },
    "createdAt": "2026-06-09T14:02:00.000Z",
    "updatedAt": "2026-06-09T14:02:00.000Z"
  }
}
Errors

Suppressions

Suppress prospects by email

POST /api/v1/prospects/suppress

Adds emails to your suppression list so those people are never contacted by any campaign. Existing prospects are marked suppressed.

Request body
Request example
{
  "emails": [
    "[email protected]",
    "[email protected]"
  ],
  "reason": "manual opt-out"
}
Response — 200
{
  "data": {
    "suppressedCount": 2,
    "alreadySuppressedCount": 0,
    "notFoundCount": 0,
    "totalProcessed": 2
  }
}
Errors

List suppression entries

GET /api/v1/suppressions

Returns suppressed emails and domains (the blocklist) with cursor pagination.

Parameters
Response — 200
{
  "data": [
    {
      "id": "sup_1a2b3c",
      "type": "email",
      "value": "[email protected]",
      "source": "api",
      "reason": "manual opt-out",
      "createdAt": "2026-06-10T12:00:00.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}

Campaigns

List campaigns

GET /api/v1/campaigns

Returns campaigns with summary stats (sent/opened/replied/...). Cursor paginated. Email and multichannel campaigns also carry `sendingMailboxes` (the mailboxes each one sends from — `state` is ready, sending_paused or needs_reconnect; `campaignDailyLimit` is the most the campaign sends from that mailbox per day; up to 50 per campaign), the exact `sendingMailboxCount` and `sendingMailboxStateCounts` (how many of them are in each state). All three are null for other channels.

Parameters
Response — 200
{
  "data": [
    {
      "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
      "name": "Q3 Outbound — Founders",
      "status": "draft",
      "channel": "email",
      "description": null,
      "timezone": "UTC",
      "dailySendLimit": 50,
      "sendingWindowStart": null,
      "sendingWindowEnd": null,
      "scheduleDays": null,
      "stopOnReply": true,
      "stopOnBounce": true,
      "stopOnUnsubscribe": true,
      "trackOpens": true,
      "trackClicks": true,
      "opt_out_style": null,
      "opt_out_text": null,
      "company_address": null,
      "totalProspects": 0,
      "sent": 0,
      "opened": 0,
      "clicked": 0,
      "replied": 0,
      "bounced": 0,
      "unsubscribed": 0,
      "startDate": null,
      "endDate": null,
      "createdAt": "2026-06-10T12:00:00.000Z",
      "updatedAt": "2026-06-10T12:00:00.000Z",
      "sendingMailboxes": [
        {
          "id": "m1b2c3d4-e5f6-47a8-9b0c-1d2e3f4a5b6c",
          "emailAddress": "[email protected]",
          "state": "ready",
          "dailyLimit": 50,
          "campaignDailyLimit": 50
        }
      ],
      "sendingMailboxCount": 1,
      "sendingMailboxStateCounts": {
        "ready": 1,
        "sending_paused": 0,
        "needs_reconnect": 0
      }
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}

Get a campaign (with steps)

GET /api/v1/campaigns/{id}

Returns a single campaign including its ordered steps array. Steps of type "condition" additionally carry conditionType, conditionTrueNext, conditionFalseNext, conditionTimeoutDays and conditions (read-only). Email and multichannel campaigns also carry `sendingMailboxes` — every mailbox the campaign sends from (up to 200), each with `state`, `dailyLimit` (the mailbox's own limit), `campaignDailyLimit` (the most this campaign sends from it per day) and `sentToday` (campaign emails it sent today, from any campaign) — plus the exact `sendingMailboxCount` and `sendingMailboxStateCounts`.

Parameters
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "draft",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "opt_out_effective": {
      "style": "link",
      "style_source": "workspace",
      "text": "Unsubscribe",
      "text_source": "default",
      "company_address": null,
      "company_address_source": "none"
    },
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      },
      {
        "id": "s2b3c4d5-e6f7-48a9-b0c1-2d3e4f5a6b7c",
        "stepIndex": 1,
        "type": "condition",
        "name": "Opened the intro?",
        "subject": null,
        "bodyHtml": null,
        "bodyText": null,
        "delayDays": 2,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "conditionType": "email_opened",
        "conditionTrueNext": 2,
        "conditionFalseNext": 3,
        "conditionTimeoutDays": 3,
        "conditions": null
      }
    ],
    "sendingMailboxes": [
      {
        "id": "m1b2c3d4-e5f6-47a8-9b0c-1d2e3f4a5b6c",
        "emailAddress": "[email protected]",
        "state": "ready",
        "dailyLimit": 50,
        "campaignDailyLimit": 50,
        "sentToday": 12
      }
    ],
    "sendingMailboxCount": 1,
    "sendingMailboxStateCounts": {
      "ready": 1,
      "sending_paused": 0,
      "needs_reconnect": 0
    }
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Why a LinkedIn campaign is (or isn't) sending

GET /api/v1/campaigns/{id}/linkedin-throttle

Explains the current sending state of a LinkedIn or multichannel campaign: which daily or weekly allowance is holding it back, when that allowance refreshes, how many campaigns share the same LinkedIn account, and this campaign's protected share of that account's allowance versus what it has used today, per action type. `state` is a stable token — one of sending, sharing_allowance, waiting_shared_allowance, waiting_daily_allowance, waiting_weekly_allowance, resting, inmail_credits_used, inmail_refused. `waiting: true` means a normal, expected wait rather than a failure. `status_headline` and `status_detail` are plain-language strings safe to show a person verbatim. Read-only: this endpoint cannot change a campaign or raise any limit.

Parameters
Response — 200
{
  "data": {
    "campaign_id": "c1a2b3c4-d5e6-4789-a012-3456789abcde",
    "campaign_name": "Q3 founders — connect",
    "campaign_status": "running",
    "linkedin_account_id": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
    "linkedin_account_name": "Jane Doe",
    "applicable": true,
    "state": "waiting_shared_allowance",
    "status_headline": "Waiting for its share",
    "status_detail": "This campaign has used its share of today's LinkedIn allowance — the allowance is split with 2 other campaigns on this account. Yours picks up again in about 7 hours.",
    "waiting": true,
    "resumes_at": "2026-07-31T00:00:00.000Z",
    "today_allowance": {
      "used": 9,
      "allowance": 25
    },
    "account_usage_today": {
      "invites": {
        "used": 29,
        "cap": 29
      },
      "messages": {
        "used": 12,
        "cap": 100
      },
      "inmails": {
        "used": 0,
        "cap": 50,
        "creditsExhausted": false
      },
      "profile_views": {
        "used": 41,
        "cap": 100
      }
    },
    "account_warmup": {
      "account_plan": "Sales Navigator",
      "pace": "Established",
      "week": 3,
      "weeks_total": 4,
      "invites_per_week": 200
    },
    "sharing": {
      "is_shared": true,
      "campaigns_on_this_account": 3,
      "other_campaigns": [
        {
          "id": "d4e5f6a7-b8c9-4012-8345-6789abcdef01",
          "name": "EU ops leads"
        },
        {
          "id": "e5f6a7b8-c9d0-4123-8456-789abcdef012",
          "name": "Warm re-engage"
        }
      ],
      "per_action": [
        {
          "action": "Invites",
          "guaranteed_share": 9,
          "used_by_this_campaign": 9,
          "used_by_account": 29,
          "account_allowance": 29,
          "held_for_other_campaigns": 0,
          "can_send_now": false
        }
      ]
    }
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Why a campaign sent little or nothing

GET /api/v1/campaigns/{id}/skipped-actions

Returns the actions that did NOT run for this campaign in a recent window, grouped by cause. Each group carries `reason` (a stable token to branch on), a short `label`, a full plain-language `plain_english` sentence safe to show a person verbatim, `count`, `first_seen_at` / `last_seen_at`, `resumes_at` when the moment is knowable, and `is_normal_wait`. `is_normal_wait: true` marks the many causes that are the system working correctly — normal pacing that resumes on its own, not a fault. `summary_headline` answers the question in one sentence. `recent` is a capped sample of individual actions, newest first. Read-only: this endpoint cannot change a campaign or raise any limit, and it never returns raw internal error text.

Parameters
Response — 200
{
  "data": {
    "window_hours": 24,
    "campaign_id": "c1a2b3c4-d5e6-4789-a012-3456789abcde",
    "summary_headline": "Nothing here is broken: everything held back in the last 24 hours is waiting for one reason — today’s allowance used — and it starts again on its own.",
    "total_skipped": 41,
    "reasons": [
      {
        "reason": "daily_allowance_reached",
        "label": "Today's allowance used",
        "count": 38,
        "plain_english": "This campaign has used everything it is allowed to send today, so the rest is queued for the next day. This is normal pacing that protects your sending reputation — it starts again on its own and there is nothing to fix.",
        "first_seen_at": "2026-08-10T09:12:04.000Z",
        "last_seen_at": "2026-08-10T17:44:51.000Z",
        "is_normal_wait": true,
        "resumes_at": "2026-08-11T00:00:00.000Z"
      },
      {
        "reason": "contact_missing_details",
        "label": "Contact details missing",
        "count": 3,
        "plain_english": "Some contacts are missing the detail this step needs, such as an email address or a profile link, so those actions were skipped and the sequence moved on. Fill in the missing details and re-add those contacts if you want to reach them.",
        "first_seen_at": "2026-08-10T10:02:11.000Z",
        "last_seen_at": "2026-08-10T10:06:33.000Z",
        "is_normal_wait": false,
        "resumes_at": null
      }
    ],
    "recent": [
      {
        "occurred_at": "2026-08-10T17:44:51.000Z",
        "channel": "linkedin",
        "action": "Invites",
        "reason": "daily_allowance_reached",
        "plain_english": "This campaign has used everything it is allowed to send today, so the rest is queued for the next day. This is normal pacing that protects your sending reputation — it starts again on its own and there is nothing to fix."
      }
    ]
  }
}
Errors

Create a campaign

POST /api/v1/campaigns

Creates a campaign in draft status with one or more steps, assigned mailboxes, and (optionally) enrolled prospects. Start it later with the start endpoint. Email, LinkedIn, WhatsApp, Instagram and mixed (multichannel) campaigns are all created here; mailboxes are only required when the campaign sends email. Creating a campaign never sends anything — every message is paced later by the scheduler within that account's safe limits.

Request body
Request example
{
  "name": "Q3 Outbound — Founders",
  "channel": "email",
  "dailySendLimit": 50,
  "mailboxIds": [
    "5bd50e20-6b75-4429-9a5a-4a09b46e945a"
  ],
  "steps": [
    {
      "stepIndex": 0,
      "type": "email",
      "subject": "Quick question, {{firstName}}",
      "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
      "delayDays": 0
    },
    {
      "stepIndex": 1,
      "type": "condition",
      "name": "Opened the intro?",
      "conditionType": "email_opened",
      "conditionTrueNext": 2,
      "conditionFalseNext": 3,
      "conditionTimeoutDays": 3,
      "delayDays": 2
    },
    {
      "stepIndex": 2,
      "type": "email",
      "subject": "Re: Quick question",
      "bodyHtml": "<p>Following up ...</p>",
      "delayDays": 3,
      "sendAsReply": true
    },
    {
      "stepIndex": 3,
      "type": "email",
      "subject": "One more idea, {{firstName}}",
      "bodyHtml": "<p>Different angle ...</p>",
      "delayDays": 3
    }
  ],
  "prospectIds": [
    "f3c4a2d1-9b7e-4c2a-8f10-2b6d5e8a1c44"
  ]
}
Response — 201
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "draft",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "opt_out_effective": {
      "style": "link",
      "style_source": "workspace",
      "text": "Unsubscribe",
      "text_source": "default",
      "company_address": null,
      "company_address_source": "none"
    },
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "prospects_enrolled": 1,
    "prospects_suppressed": 0,
    "mailboxes_assigned": 1
  }
}
Errors

Update a campaign

PATCH /api/v1/campaigns/{id}

Updates campaign settings. Only allowed while the campaign is in draft or paused status.

Parameters
Request body
Request example
{
  "dailySendLimit": 80,
  "scheduleDays": [
    1,
    2,
    3,
    4,
    5
  ]
}
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "draft",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 80,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": [
      1,
      2,
      3,
      4,
      5
    ],
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "opt_out_effective": {
      "style": "link",
      "style_source": "workspace",
      "text": "Unsubscribe",
      "text_source": "default",
      "company_address": null,
      "company_address_source": "none"
    },
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Start a campaign

POST /api/v1/campaigns/{id}/start

Moves a draft/paused campaign into running (or scheduled, when startDate is in the future). A completed campaign starts again only when new prospects have been enrolled in it (otherwise 400 nothing_new_to_send). No request body. Before starting, the campaign runs the same configuration check as the Launch button in the app — sequences that would strand prospects mid-flow are rejected with 400 campaign_validation_failed and the offending steps listed.

Parameters
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "running",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Pause a campaign

POST /api/v1/campaigns/{id}/pause

Pauses a running campaign. No request body. Pausing is never blocked by seats or by a disconnected sender — stopping your own outreach always works.

Parameters
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "paused",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Archive a campaign

POST /api/v1/campaigns/{id}/archive

Hides a paused, completed or draft campaign from the campaign list. Nothing is deleted: steps, settings, contacts and results are kept, and you can bring it back any time with the unarchive call. List archived campaigns with GET /campaigns?archived=true. No request body.

Parameters
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "completed",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "archived": true,
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Unarchive a campaign

POST /api/v1/campaigns/{id}/unarchive

Brings an archived campaign back to your campaign list as paused. It never starts sending on its own: start it with POST /campaigns/{id}/start when you are ready. No request body.

Parameters
Response — 200
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "paused",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "opt_out_style": null,
    "opt_out_text": null,
    "company_address": null,
    "totalProspects": 0,
    "sent": 0,
    "opened": 0,
    "clicked": 0,
    "replied": 0,
    "bounced": 0,
    "unsubscribed": 0,
    "startDate": null,
    "endDate": null,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:00:00.000Z",
    "archived": false,
    "steps": [
      {
        "id": "s1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
        "stepIndex": 0,
        "type": "email",
        "name": "Intro",
        "subject": "Quick question, {{firstName}}",
        "bodyHtml": "<p>Hi {{firstName}}, ...</p>",
        "bodyText": "Hi {{firstName}}, ...",
        "delayDays": 0,
        "delayHours": null,
        "isActive": true,
        "sendAsReply": null,
        "quotePreviousMessages": true,
        "sentCount": 0,
        "openCount": 0,
        "clickCount": 0,
        "replyCount": 0,
        "bounceCount": 0,
        "linkedinActionType": null,
        "linkedinMessageTemplate": null,
        "linkedinInviteNote": null,
        "linkedinSubject": null,
        "linkedinMaxWaitDays": null,
        "linkedinEngagementConfig": null,
        "linkedinInmailConfig": null
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Enrollments

Enroll prospects

POST /api/v1/campaigns/{id}/enrollments

Adds prospects to a campaign by prospect id, audience list id, email, and/or WhatsApp number. Suppressed prospects are skipped and counted separately. Provide at least one of prospectIds, listIds, emails, whatsappPhones. Emails and numbers that match no existing lead are reported back rather than silently dropped. Enrolling new prospects into a completed campaign reopens it for them (campaignReopened: true) — prospects already in it, including those who finished, are skipped and never emailed again, and sending is paced by the scheduler as usual.

Parameters
Request body
Request example
{
  "emails": [
    "[email protected]"
  ],
  "listIds": [
    "list_9f3a2b"
  ],
  "whatsappPhones": [
    "+49 171 2345678"
  ]
}
Response — 200
{
  "data": {
    "enrolledCount": 12,
    "suppressedCount": 1,
    "invalidCount": 0,
    "alreadyEnrolledCount": 2,
    "totalProcessed": 15,
    "notFoundEmails": [],
    "notFoundPhones": [],
    "campaignStatus": "running",
    "campaignReopened": false
  }
}
Errors

Unenroll prospects

DELETE /api/v1/campaigns/{id}/enrollments

Removes prospects from a campaign (soft-stop — their audit history is preserved and any pending sends are cancelled). Provide at least one of prospectIds, emails.

Parameters
Request body
Request example
{
  "emails": [
    "[email protected]"
  ]
}
Response — 200
{
  "data": {
    "unenrolledCount": 1,
    "totalProcessed": 1
  }
}
Errors

Sent emails

List sent campaign emails

GET /api/v1/emails/sent

One record per campaign email your mailboxes sent: who it went to, the campaign and step, the From address, the subject, the Message-ID and when it was sent. Each record has exactly the same fields as the email.sent webhook, so you can backfill any event your endpoint missed. "Sent" means the email was accepted by your sending mailbox's server — not yet a delivery confirmation. Problems found later arrive as the email.bounced webhook. Newest first unless you pass order=asc. Cursor paginated.

Parameters
Response — 200
{
  "data": [
    {
      "prospectId": "f3c4a2d1-...",
      "prospectEmail": "[email protected]",
      "campaignId": "c1a2b3c4-...",
      "campaignName": "Q3 founders outreach",
      "stepIndex": 1,
      "stepNumber": 2,
      "mailboxId": "5bd50e20-...",
      "fromEmail": "[email protected]",
      "subject": "Quick question, Jane",
      "messageId": "<[email protected]>",
      "sentAt": "2026-06-10T10:42:00.000Z",
      "channel": "email"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Replies

List email replies

GET /api/v1/replies

The replies your email campaigns received, newest first, with the reply text. Each record has the same fields as the reply.received webhook (plus an id, the conversation id and the sender's name), so you can backfill any reply your endpoint missed or read the text of a reply later. bodyText holds the text of the reply as it arrived (up to 4,000 characters; bodyTruncated is true when it was cut). Cursor paginated. Read-only.

Parameters
Response — 200
{
  "data": [
    {
      "id": "7d1e4c9a-...",
      "threadId": "2b8f6a10-...",
      "prospectId": "f3c4a2d1-...",
      "prospectEmail": "[email protected]",
      "campaignId": "c1a2b3c4-...",
      "campaignName": "Q3 founders outreach",
      "mailboxId": "5bd50e20-...",
      "fromEmail": "[email protected]",
      "fromName": "Jane Cooper",
      "toEmail": "[email protected]",
      "subject": "Re: Quick question, Jane",
      "messageId": "<[email protected]>",
      "inReplyToMessageId": "<[email protected]>",
      "receivedAt": "2026-06-10T11:00:00.000Z",
      "bodyText": "Hi Alex, yes, send me the details on Thursday.",
      "bodyTruncated": false,
      "channel": "email"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Get one email reply

GET /api/v1/replies/{id}

One reply by its id, with the same fields as the list plus bodyHtml, the formatted version of the message (up to 20,000 characters; bodyHtmlTruncated is true when it was cut).

Parameters
Response — 200
{
  "data": {
    "id": "7d1e4c9a-...",
    "threadId": "2b8f6a10-...",
    "prospectId": "f3c4a2d1-...",
    "prospectEmail": "[email protected]",
    "campaignId": "c1a2b3c4-...",
    "campaignName": "Q3 founders outreach",
    "mailboxId": "5bd50e20-...",
    "fromEmail": "[email protected]",
    "fromName": "Jane Cooper",
    "toEmail": "[email protected]",
    "subject": "Re: Quick question, Jane",
    "messageId": "<[email protected]>",
    "inReplyToMessageId": "<[email protected]>",
    "receivedAt": "2026-06-10T11:00:00.000Z",
    "bodyText": "Hi Alex, yes, send me the details on Thursday.",
    "bodyTruncated": false,
    "channel": "email",
    "bodyHtml": "<div>Hi Alex, yes, send me the details on Thursday.</div>",
    "bodyHtmlTruncated": false
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Inbox

List inbox conversations

GET /api/v1/inbox/conversations

Your Unified Inbox: replies to your cold emails plus LinkedIn, Instagram and WhatsApp chats, newest first, in one feed. Each row says who the conversation is with, what was last said and by whom, whether it needs a reply, the campaign it came from and the mailbox or account on your side. Filter to one channel, to conversations that need a reply, to unread ones, by status, sentiment, campaign, mailbox, connected account or prospect, or search by name, address, company, subject and last message. Cursor paginated. Reading never spends any sending limit.

Parameters
Response — 200
{
  "data": [
    {
      "id": "8f2dc14c-9b1e-4a7a-8f0e-2b6c1d3e4f50",
      "channel": "email",
      "participant": {
        "name": "Jane Doe",
        "email": "[email protected]",
        "handle": null,
        "phone": null,
        "company": "Acme",
        "profile_url": null
      },
      "subject": "Re: Quick question, Jane",
      "last_message_at": "2026-10-14T08:10:00.000Z",
      "last_message_preview": "Yes — can you send a deck?",
      "last_message_from": "them",
      "needs_reply": true,
      "unread": true,
      "unread_count": 1,
      "message_count": 2,
      "status": "open",
      "sentiment": "positive",
      "starred": false,
      "assigned_to": null,
      "is_group": false,
      "prospect_id": "f3c4a2d1-...",
      "campaign_id": "c1a2b3c4-...",
      "campaign_name": "Q3 founders outreach",
      "account": {
        "label": "[email protected]",
        "mailbox_id": "5bd50e20-...",
        "linkedin_account_id": null,
        "instagram_account_id": null,
        "whatsapp_account_id": null
      },
      "created_at": "2026-10-13T09:02:00.000Z"
    }
  ],
  "pagination": {
    "has_more": true,
    "next_cursor": "MTc1OTk5OTk5OTAwMHw4ZjJkYzE0Yy0..."
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Read one conversation

GET /api/v1/inbox/conversations/{channel}/{id}

One conversation in full: every message in the order it happened, who wrote it, when it was sent and (for chats) when it was delivered, read or edited, plus the internal notes your team left on an email thread. Does not mark the conversation read.

Parameters
Response — 200
{
  "data": {
    "conversation": {
      "id": "8f2dc14c-9b1e-4a7a-8f0e-2b6c1d3e4f50",
      "channel": "email",
      "participant": {
        "name": "Jane Doe",
        "email": "[email protected]",
        "handle": null,
        "phone": null,
        "company": "Acme",
        "profile_url": null
      },
      "subject": "Re: Quick question, Jane",
      "last_message_at": "2026-10-14T08:10:00.000Z",
      "last_message_preview": "Yes — can you send a deck?",
      "last_message_from": "them",
      "needs_reply": true,
      "unread": true,
      "unread_count": 1,
      "message_count": 2,
      "status": "open",
      "sentiment": "positive",
      "starred": false,
      "assigned_to": null,
      "is_group": false,
      "prospect_id": "f3c4a2d1-...",
      "campaign_id": "c1a2b3c4-...",
      "campaign_name": "Q3 founders outreach",
      "account": {
        "label": "[email protected]",
        "mailbox_id": "5bd50e20-...",
        "linkedin_account_id": null,
        "instagram_account_id": null,
        "whatsapp_account_id": null
      },
      "created_at": "2026-10-13T09:02:00.000Z"
    },
    "messages": [
      {
        "id": "c0a1b2c3-...",
        "from": "you",
        "from_name": "Jordan Lee",
        "from_address": "[email protected]",
        "subject": "Quick question, Jane",
        "text": "Hi Jane, quick question — ...",
        "sent_at": "2026-10-13T09:02:00.000Z",
        "delivered_at": null,
        "read_at": null,
        "edited_at": null,
        "deleted_at": null,
        "reactions": null,
        "attachments": null,
        "is_voice_note": false
      },
      {
        "id": "d1b2c3d4-...",
        "from": "them",
        "from_name": "Jane Doe",
        "from_address": "[email protected]",
        "subject": "Re: Quick question, Jane",
        "text": "Yes — can you send a deck?",
        "sent_at": "2026-10-14T08:10:00.000Z",
        "delivered_at": null,
        "read_at": null,
        "edited_at": null,
        "deleted_at": null,
        "reactions": null,
        "attachments": null,
        "is_voice_note": false
      }
    ],
    "notes": [
      {
        "id": "e2c3d4e5-...",
        "body": "Wants a deck — send Thursday.",
        "author_name": "Jordan Lee",
        "created_at": "2026-10-14T08:20:00.000Z"
      }
    ]
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Triage an email conversation

PATCH /api/v1/inbox/conversations/{id}

Change how an email conversation is filed: status (open, closed, snoozed with snoozeUntil), sentiment, unread, starred, or assignedTo (a workspace member's user id, or null). Closing a conversation, or labelling it out-of-office or spam, removes it from Needs Reply; a new message from the contact reopens it. Labelling ooo resumes the campaign for that contact. Nothing is sent. LinkedIn, Instagram and WhatsApp chats cannot be changed here.

Parameters
Request body
Request example
{
  "status": "closed",
  "sentiment": "positive"
}
Response — 200
{
  "data": {
    "id": "8f2dc14c-9b1e-4a7a-8f0e-2b6c1d3e4f50",
    "channel": "email",
    "participant": {
      "name": "Jane Doe",
      "email": "[email protected]",
      "handle": null,
      "phone": null,
      "company": "Acme",
      "profile_url": null
    },
    "subject": "Re: Quick question, Jane",
    "last_message_at": "2026-10-14T08:10:00.000Z",
    "last_message_preview": "Yes — can you send a deck?",
    "last_message_from": "them",
    "needs_reply": false,
    "unread": true,
    "unread_count": 1,
    "message_count": 2,
    "status": "closed",
    "sentiment": "positive",
    "starred": false,
    "assigned_to": null,
    "is_group": false,
    "prospect_id": "f3c4a2d1-...",
    "campaign_id": "c1a2b3c4-...",
    "campaign_name": "Q3 founders outreach",
    "account": {
      "label": "[email protected]",
      "mailbox_id": "5bd50e20-...",
      "linkedin_account_id": null,
      "instagram_account_id": null,
      "whatsapp_account_id": null
    },
    "created_at": "2026-10-13T09:02:00.000Z"
  },
  "changed": [
    "status",
    "sentiment"
  ]
}
Errors

Add an internal note

POST /api/v1/inbox/conversations/{id}/notes

Leave an internal note on an email conversation for your team. Notes show in the inbox side panel and on GET; the contact never sees them. Up to 5,000 characters.

Parameters
Request body
Request example
{
  "body": "Qualified — wants a demo next week. Handing to Sam."
}
Response — 201
{
  "data": {
    "id": "e2c3d4e5-...",
    "body": "Qualified — wants a demo next week. Handing to Sam.",
    "created_at": "2026-10-14T08:20:00.000Z"
  }
}
Errors

Webhooks

List webhooks

GET /api/v1/webhooks

Returns your configured webhook endpoints (without secrets).

Response — 200
{
  "data": [
    {
      "id": "wh_2c1a9f",
      "url": "https://example.com/hooks/warmysender",
      "events": [
        "reply.received",
        "email.bounced"
      ],
      "isActive": true,
      "lastTriggeredAt": null,
      "failureCount": 0,
      "createdAt": "2026-06-10T12:00:00.000Z",
      "updatedAt": "2026-06-10T12:00:00.000Z"
    }
  ]
}

Create a webhook

POST /api/v1/webhooks

Registers a webhook endpoint for one or more event types. The signing secret (whsec_…) is returned ONCE in this response — store it; you'll use it to verify payload signatures.

Request body
Request example
{
  "url": "https://example.com/hooks/warmysender",
  "events": [
    "reply.received",
    "email.bounced",
    "linkedin.reply_received"
  ]
}
Response — 201
{
  "data": {
    "id": "wh_2c1a9f",
    "url": "https://example.com/hooks/warmysender",
    "events": [
      "reply.received",
      "email.bounced",
      "linkedin.reply_received"
    ],
    "isActive": true,
    "secret": "whsec_3f9a...redacted",
    "createdAt": "2026-06-10T12:00:00.000Z"
  },
  "message": "Webhook created. Save the secret - it will not be shown again."
}
Errors

Update a webhook

PATCH /api/v1/webhooks/{id}

Change a webhook's URL, subscribed events, or active flag.

Parameters
Request body
Request example
{
  "isActive": false
}
Response — 200
{
  "data": {
    "id": "wh_2c1a9f",
    "url": "https://example.com/hooks/warmysender",
    "events": [
      "reply.received"
    ],
    "isActive": false,
    "lastTriggeredAt": null,
    "failureCount": 0,
    "createdAt": "2026-06-10T12:00:00.000Z",
    "updatedAt": "2026-06-10T12:05:00.000Z"
  }
}
Errors

Delete a webhook

DELETE /api/v1/webhooks/{id}

Permanently removes a webhook endpoint. Returns 204 with no body.

Parameters
Response — 204

No response body.

Errors

Send a test event

POST /api/v1/webhooks/{id}/test

Queues a webhook.test delivery to the endpoint so you can verify signature handling end-to-end. No request body.

Parameters
Response — 200
{
  "data": {
    "eventId": "evt_a1b2c3",
    "status": "queued",
    "message": "Test webhook event has been queued for delivery"
  }
}
Errors

LinkedIn

List LinkedIn accounts

GET /api/v1/linkedin/accounts

Returns connected LinkedIn accounts with their daily engagement limits and how many actions remain today.

Parameters
Response — 200
{
  "data": [
    {
      "id": "li_5a2b1c",
      "name": "Jordan Lee",
      "linkedin_url": "https://www.linkedin.com/in/jordanlee",
      "status": "connected",
      "linkedin_account_type": "classic",
      "strategy": "steady",
      "ramp_week": 3,
      "engagements_today": 4,
      "engagements_day": "2026-06-10",
      "created_at": "2026-05-20T10:00:00.000Z",
      "daily_limits": {
        "engagements_per_day": 30,
        "engagements_remaining": 26
      }
    }
  ]
}

React to a post

POST /api/v1/linkedin/posts/{postId}/reactions

Adds a reaction (like, celebrate, etc.) to a LinkedIn post from one of your connected accounts. Subject to per-account daily engagement limits — account safety always wins.

Parameters
Request body
Request example
{
  "linkedin_account_id": "li_5a2b1c",
  "reaction_type": "LIKE"
}
Response — 201
{
  "data": {
    "post_id": "urn:li:activity:7300000000000000000",
    "reaction_type": "LIKE",
    "linkedin_account_id": "li_5a2b1c"
  }
}
Errors

List reactions on a post

GET /api/v1/linkedin/posts/{postId}/reactions

Lists who reacted to a post, as seen by one of your connected accounts. Cursor paginated.

Parameters
Response — 200
{
  "data": [
    {
      "user_id": "urn:li:person:abc",
      "name": "Sam Rivera",
      "reaction_type": "LIKE",
      "timestamp": "2026-06-10T11:00:00.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Errors

Verification

Verify one email address

POST /api/v1/verification/verify

Checks a single address and returns one of four outcomes with a plain-English reason. Note that a confirmed mailbox is only one of them — on a mail server that accepts every address the outcome is `risky`, which is deliverable-but-unconfirmable rather than bad. Answers we cannot confirm are always free.

Request body
Request example
{
  "email": "[email protected]"
}
Response — 200
{
  "data": {
    "email": "[email protected]",
    "status": "valid",
    "reason": "The mail provider accepted this address.",
    "is_catch_all": false,
    "mx_host": "aspmx.l.google.com"
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Start a verification run

POST /api/v1/verification/batches

Submits a list of addresses to be checked in the background and returns a run id to poll. Pass either an `emails` array or a `list_id` to check one of your prospect lists in place. A run starts in full or not at all: if the list is larger than what your allowance and credits cover today, the whole request is refused with `exceeds_today` and nothing is charged — the error says how many can start today, so split the list to that size or add credits. When the run finishes, the addresses that are ready for a campaign are saved as a list named "<run name> - Verified" — pass `create_list: false` with `emails` if you keep the results in your own system and do not want that list. A `list_id` run works exactly like Verify list in the app: contacts on that list whose address turns out invalid are marked invalid so campaigns stop emailing them, and the campaign-ready contacts are saved as "<list name> - Verified".

Request body
Request example
{
  "emails": [
    "[email protected]",
    "[email protected]"
  ],
  "name": "Q3 outbound list",
  "create_list": false
}
Response — 202
{
  "data": {
    "id": "b1a2c3d4-...",
    "status": "processing",
    "continues_tomorrow": false,
    "accepted": 2,
    "submitted": 2,
    "create_list": false
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

List verification runs

GET /api/v1/verification/batches

Lists this workspace's verification runs, newest first, with their progress and result counts.

Parameters
Response — 200
{
  "data": [
    {
      "id": "b1a2c3d4-...",
      "name": "Q3 outbound list",
      "status": "completed",
      "source": "upload",
      "total": 2,
      "checked": 2,
      "valid": 1,
      "invalid": 1,
      "risky": 0,
      "unknown": 0,
      "created_at": "2026-07-29T09:00:00.000Z",
      "started_at": "2026-07-29T09:00:01.000Z",
      "completed_at": "2026-07-29T09:04:12.000Z",
      "create_list": true
    }
  ],
  "pagination": {
    "has_more": false
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Get a verification run

GET /api/v1/verification/batches/{id}

Returns the progress and result counts for one verification run.

Parameters
Response — 200
{
  "data": {
    "id": "b1a2c3d4-...",
    "name": "Q3 outbound list",
    "status": "processing",
    "source": "upload",
    "total": 2000,
    "checked": 640,
    "valid": 512,
    "invalid": 96,
    "risky": 24,
    "unknown": 8,
    "created_at": "2026-07-29T09:00:00.000Z",
    "started_at": "2026-07-29T09:00:01.000Z",
    "completed_at": null,
    "create_list": true
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Get per-address results

GET /api/v1/verification/batches/{id}/results

Returns the outcome for each address in a run, optionally filtered to one outcome. Results are available while the run is still going, so you can act on the part that has finished.

Parameters
Response — 200
{
  "data": [
    {
      "email": "[email protected]",
      "status": "valid",
      "reason": "The mail provider accepted this address.",
      "is_catch_all": false,
      "mx_host": "aspmx.l.google.com",
      "checked_at": "2026-07-29T09:01:44.000Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "total": 1,
    "limit": 100,
    "offset": 0
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Stop a run and refund unused credits

POST /api/v1/verification/batches/{id}/cancel

Stops a verification run that is still going and returns the credits that were paid for but never used. Addresses already checked keep their results.

Parameters
Response — 200
{
  "data": {
    "id": "b1a2c3d4-...",
    "status": "cancelled",
    "cancelled": true,
    "stopped_addresses": 1360,
    "credits_refunded": 1360,
    "credit_balance": 4360
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Build a prospect list from results

POST /api/v1/verification/batches/{id}/create-list

Builds a reusable prospect list from a finished run. By default it keeps two kinds of address: ones whose mailbox was confirmed, and ones on a mail server that accepts every address. Pass keep=valid for confirmed-only, or keep=risky for catch-all only. The default mix is on purpose — many catch-all addresses still deliver — and the response reports the split so you can decide how to send.

Parameters
Request body
Request example
{
  "keep": "valid"
}
Response — 201
{
  "data": {
    "created": true,
    "list_id": "l9f8e7d6-...",
    "list_name": "Q3 outbound list (verified) — confirmed only",
    "address_count": 604,
    "confirmed_count": 604,
    "unconfirmable_count": 0,
    "keep": "valid"
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Check remaining verification allowance

GET /api/v1/verification/allowance

Returns whether this account can check an address right now and what would pay for it — the plan allowance left this month and today, any monthly add-on allowance (addon_monthly_allowance / addon_remaining — not paced daily), the purchased credit balance, free checks left today, and the largest run accepted. Read this before submitting so you never discover a limit by being refused.

Response — 200
{
  "data": {
    "can_verify_now": true,
    "access_source": "plan",
    "monthly_allowance": 10000,
    "monthly_used": 1240,
    "monthly_remaining": 8760,
    "daily_allowance": 333,
    "daily_used": 40,
    "daily_remaining": 293,
    "credit_balance": 5000,
    "addon_monthly_allowance": 0,
    "addon_remaining": 0,
    "free_checks_remaining_today": 0,
    "can_buy_credits": true,
    "verifiable_now": 5293,
    "max_addresses_per_batch": 5000
  },
  "meta": {
    "request_id": "req_lz3k9a2b"
  }
}
Errors

Webhook events

WarmySender posts real-time webhook notifications for key events. Configure them from Settings → Webhooks or via the API. Each delivery is signed with HMAC-SHA256 (X-Warmy-Signature) and retried with exponential backoff until your endpoint returns a 2xx. Subscribe to any of the event types below.

reply.received

A prospect replied (email or LinkedIn). messageId is the Message-ID of the reply itself. For email, inReplyToMessageId is the messageId of the email it answered (the same value as in email.sent and GET /emails/sent), plus the stepIndex and stepNumber of that email; these three fields are left out when the reply does not name one of your emails. For email, bodyText is the text of the reply (up to 4,000 characters, with bodyTruncated telling you when it was cut) and fromEmail is the address it came from; GET /replies returns the same record for backfilling.

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "mailboxId": "5bd50e20-...",
  "subject": "Re: Quick question, Jane",
  "messageId": "<[email protected]>",
  "receivedAt": "2026-06-10T11:00:00.000Z",
  "channel": "email",
  "inReplyToMessageId": "<[email protected]>",
  "stepIndex": 1,
  "stepNumber": 2,
  "fromEmail": "[email protected]",
  "bodyText": "Hi Alex, yes, send me the details on Thursday.",
  "bodyTruncated": false
}

email.sent

A campaign email was accepted by your sending mailbox server. This is not yet a delivery confirmation: problems found later arrive as email.bounced. Warmup emails and test sends are not included. Same fields as GET /emails/sent.

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "campaignName": "Q3 founders outreach",
  "stepIndex": 1,
  "stepNumber": 2,
  "mailboxId": "5bd50e20-...",
  "fromEmail": "[email protected]",
  "subject": "Quick question, Jane",
  "messageId": "<[email protected]>",
  "sentAt": "2026-06-10T10:42:00.000Z",
  "channel": "email"
}

email.bounced

An outbound email bounced (hard or soft).

{
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "mailboxId": "5bd50e20-...",
  "bounceType": "hard",
  "bounceReason": "550 user unknown",
  "bouncedAt": "2026-06-10T11:01:00.000Z"
}

email.unsubscribed

A prospect unsubscribed.

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "unsubscribedAt": "2026-06-10T11:02:00.000Z"
}

email.opened

A tracked email was opened.

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "stepIndex": 0,
  "openedAt": "2026-06-10T11:03:00.000Z"
}

email.clicked

A tracked link was clicked.

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "url": "https://yourdomain.com/demo",
  "clickedAt": "2026-06-10T11:04:00.000Z"
}

prospect.suppressed

A prospect was added to the suppression list.

{
  "prospectId": "f3c4a2d1-...",
  "email": "[email protected]",
  "reason": "manual opt-out",
  "suppressedAt": "2026-06-10T11:05:00.000Z"
}

limit.hit

A sending limit (mailbox/domain/prospect/cooldown) was reached.

{
  "limitType": "daily_mailbox",
  "mailboxId": "5bd50e20-...",
  "currentValue": 50,
  "limitValue": 50,
  "hitAt": "2026-06-10T11:06:00.000Z"
}

linkedin.invite_accepted

A LinkedIn connection invite was accepted. Sent once per person per campaign. lateAccept is true when the person accepted after their last step in the campaign had already run (for example on an invite-only campaign, or after the wait-for-acceptance window); lateAcceptObservedAt is when we saw it. Nothing extra is sent on LinkedIn because of this event.

{
  "prospectName": "Sam Rivera",
  "prospectLinkedinUrl": "https://www.linkedin.com/in/samrivera",
  "campaignId": "c1a2b3c4-...",
  "enrollmentId": "enr_...",
  "linkedinAccountId": "li_5a2b1c",
  "acceptedAt": "2026-06-10T11:07:00.000Z",
  "lateAccept": false,
  "lateAcceptObservedAt": null
}

linkedin.reply_received

A LinkedIn message reply was received.

{
  "prospectName": "Sam Rivera",
  "prospectLinkedinUrl": "https://www.linkedin.com/in/samrivera",
  "campaignId": "c1a2b3c4-...",
  "enrollmentId": "enr_...",
  "linkedinAccountId": "li_5a2b1c",
  "messageText": "Sure, let's talk",
  "threadId": "th_...",
  "receivedAt": "2026-06-10T11:08:00.000Z"
}

instagram.reply_received

An Instagram DM reply was received.

{
  "prospectName": "Sam Rivera",
  "instagramProfileUrl": "https://instagram.com/samrivera",
  "instagramUsername": "samrivera",
  "campaignId": "c1a2b3c4-...",
  "enrollmentId": "enr_...",
  "instagramAccountId": "ig_5a2b1c",
  "messageText": "Sure, send it over",
  "threadId": "th_...",
  "receivedAt": "2026-06-10T11:08:30.000Z"
}

whatsapp.reply_received

A WhatsApp reply was received.

{
  "prospectName": "Sam Rivera",
  "whatsappPhone": "491712345678",
  "campaignId": "c1a2b3c4-...",
  "enrollmentId": "enr_...",
  "whatsappAccountId": "wa_5a2b1c",
  "messageText": "Yes, go ahead",
  "chatId": "ch_...",
  "receivedAt": "2026-06-10T11:08:45.000Z"
}

webhook.test

A manual test event sent from the test endpoint.

{
  "webhookId": "wh_2c1a9f",
  "timestamp": "2026-06-10T11:09:00.000Z",
  "message": "This is a test event from WarmySender"
}

Common errors

Errors return a JSON body with a stable code and a human-readable message. These apply across all endpoints (individual endpoints list their own additional errors above).

Native Slack integration (no middleware)

Paste a Slack Incoming Webhook URL (https://hooks.slack.com/services/…) into Settings → Webhooks and we auto-format each event as a Block Kit message. LinkedIn reply notifications include the reply body, prospect name, profile URL, and campaign — rendered as a readable Slack card. No Zapier, Make, or n8n required in the middle. Slack rate limits are honored automatically via the Retry-After header Slack returns when throttling.

n8n, Zapier, Make integration

Create an HTTP Trigger (Webhook) node in n8n (or a Webhook trigger in Zapier / Make), copy its Production URL, and paste it into Settings → Webhooks. You receive the full JSON payload and can fan out to any downstream system — Slack, Discord, Teams, HubSpot, Pipedrive, Airtable, Google Sheets, Notion, or a custom database — with conditional logic and transformations between steps. Signature verification via X-Warmy-Signature is recommended when your endpoint is publicly reachable.

Popular Integrations

Full Documentation | Pricing