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. Useful for verifying a key and discovering its permissions. 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"
  }
}
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.

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,
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_progress": 62,
      "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"
    }
  ],
  "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".

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,
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "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

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,
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 0,
    "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,
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "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,
    "warmup_enabled": false,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "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,
    "warmup_enabled": false,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "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 SMTP + IMAP connection test against the stored credentials and updates the mailbox status. Rate limited.

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,
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_progress": 62,
      "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
    }
  },
  "meta": {
    "request_id": "req_mq28denhy91fh6"
  }
}
Errors

Warmup

List warmup stats

GET /api/v1/warmup/stats

Per-mailbox warmup health: progress, today's volume, inbox/spam rate, reputation. Cursor paginated.

Parameters
Response — 200
{
  "data": [
    {
      "mailbox_id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
      "email_address": "[email protected]",
      "warmup_enabled": true,
      "warmup_type": "normal",
      "warmup_progress": 62,
      "warmup_target_daily_volume": 50,
      "effective_daily_limit": 50,
      "effective_limit_reason": null,
      "sent_today": 18,
      "received_today": 17,
      "reputation_score": 96,
      "inbox_rate": 98,
      "spam_rate": 1,
      "last_sent_at": "2026-06-10T09:12:00.000Z",
      "health_status": "ok",
      "health_reasons": null
    }
  ],
  "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.

Parameters
Response — 200
{
  "data": {
    "mailbox_id": "5bd50e20-6b75-4429-9a5a-4a09b46e945a",
    "email_address": "[email protected]",
    "warmup_enabled": true,
    "warmup_type": "normal",
    "warmup_progress": 62,
    "warmup_target_daily_volume": 50,
    "effective_daily_limit": 50,
    "effective_limit_reason": null,
    "sent_today": 18,
    "received_today": 17,
    "reputation_score": 96,
    "inbox_rate": 98,
    "spam_rate": 1,
    "last_sent_at": "2026-06-10T09:12:00.000Z",
    "health_status": "ok",
    "health_reasons": null
  },
  "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".

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_progress": 62,
    "warmup_target_daily_volume": 50,
    "effective_daily_limit": 50,
    "effective_limit_reason": null,
    "sent_today": 18,
    "received_today": 17,
    "reputation_score": 96,
    "inbox_rate": 98,
    "spam_rate": 1,
    "last_sent_at": "2026-06-10T09:12:00.000Z",
    "health_status": "ok",
    "health_reasons": null
  },
  "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.

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 with cursor pagination.

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

Create a prospect

POST /api/v1/prospects

Creates a prospect. If one with the same email already exists it is merged and returned with alreadyExisted=true (HTTP 200); a brand-new prospect returns 201.

Request body
Request example
{
  "email": "[email protected]",
  "firstName": "Jane",
  "lastName": "Doe",
  "company": "Acme Inc",
  "linkedinUrl": "https://www.linkedin.com/in/janedoe",
  "customFields": {
    "plan": "enterprise"
  }
}
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",
    "customFields": {
      "plan": "enterprise"
    },
    "createdAt": "2026-06-09T14:02:00.000Z",
    "updatedAt": "2026-06-09T14:02:00.000Z"
  },
  "alreadyExisted": false
}
Errors

Update a prospect

PATCH /api/v1/prospects/{id}

Partial update of a prospect. linkedinUrl may be set or cleared (null). globalStatus can be changed (e.g. to mark unsubscribed).

Parameters
Request body
Request example
{
  "role": "VP Engineering",
  "globalStatus": "active"
}
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",
    "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.

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,
      "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"
    }
  ],
  "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.

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,
    "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,
        "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

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.

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": "email",
      "subject": "Re: Quick question",
      "bodyHtml": "<p>Following up ...</p>",
      "delayDays": 3,
      "sendAsReply": true
    }
  ],
  "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,
    "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,
        "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,
    "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,
        "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. No request body.

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,
    "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,
        "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.

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,
    "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,
        "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, and/or email. Suppressed prospects are skipped and counted separately. Provide at least one of prospectIds, listIds, emails.

Parameters
Request body
Request example
{
  "emails": [
    "[email protected]"
  ],
  "listIds": [
    "list_9f3a2b"
  ]
}
Response — 200
{
  "data": {
    "enrolledCount": 12,
    "suppressedCount": 1,
    "invalidCount": 0,
    "alreadyEnrolledCount": 2,
    "totalProcessed": 15
  }
}
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

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

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

{
  "prospectId": "f3c4a2d1-...",
  "prospectEmail": "[email protected]",
  "campaignId": "c1a2b3c4-...",
  "mailboxId": "5bd50e20-...",
  "subject": "Re: Quick question",
  "messageId": "<...>",
  "receivedAt": "2026-06-10T11:00: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.

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

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

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