Complete REST API documentation for WarmySender. Build custom integrations with our cold email, email warmup, campaign, and LinkedIn automation APIs.
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.
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
All API endpoints are available at:
https://warmysender.com/api/v1
API requests are limited to 60 requests per minute per workspace. Rate limit headers are included in all responses:
X-RateLimit-Limit — Maximum requests per windowX-RateLimit-Remaining — Remaining requests in current windowX-RateLimit-Reset — Unix timestamp when the window resetsRetry-After — Seconds to wait (only on 429 responses)API keys are scoped to your workspace with granular permissions. Each endpoint lists the scope it needs.
mailboxes:read — List and read connected mailboxes.mailboxes:write — Create, update, test, delete, and restore mailboxes.warmup:read — Read warmup stats and per-mailbox warmup health.warmup:write — Enable/disable warmup and change warmup strategy or volume.jobs:read — Read the status of asynchronous jobs.prospects:read — List and read prospects.prospects:write — Create and update prospects.campaigns:read — List and read campaigns and their steps.campaigns:write — Create, update, start, and pause campaigns.enrollments:write — Enroll and unenroll prospects in campaigns.suppressions:read — Read the suppression (blocklist) entries.suppressions:write — Suppress prospects so they are never contacted.webhooks:read — List configured webhook endpoints.webhooks:write — Create, update, delete, and test webhook endpoints.linkedin:read — Read connected LinkedIn accounts and post reactions.linkedin:write — Perform LinkedIn engagement actions (e.g. react to posts).verification:read — Read email-verification batch results.verification:write — Verify email addresses (single checks and batches).leads:read — Search the leads database (masked results — no contact details).leads:write — Save or export leads (reveals contact details; uses your allowance or lead credits).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.
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.
{
"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"
}
}
unauthorized — Missing or invalid API key.GET /api/v1/mailboxes
Returns connected mailboxes, newest first, with cursor pagination. Soft-deleted mailboxes are excluded unless you filter by status=deleted.
mailboxes:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 50 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.external_id — in: query — type: string — optional. Exact-match filter on your external_id.email_address — in: query — type: string — optional. Case-insensitive substring match on the email address.status — in: query — type: string — optional — one of: created, testing, connected, error, deleted. Filter by status.warmup_enabled — in: query — type: string — optional — one of: true, false. Filter by warmup on/off.{
"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 /api/v1/mailboxes/{id}
Fetch a single mailbox by id. Poll this after create/restore until status becomes "connected".
mailboxes:readid — in: path — type: string — required. Mailbox id (UUID).{
"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"
}
}
not_found — No mailbox with that id in your workspace.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).
mailboxes:writeemail_address — type: string — required — format: email. The mailbox email address.smtp_host — type: string — required. SMTP server hostname, e.g. smtp.gmail.com.smtp_port — type: integer — optional — default: 587 — range: 1–65535. SMTP port. One of 25, 465, 587. Defaults to 587.smtp_username — type: string — required. SMTP login (usually the email address).smtp_password — type: string — required — format: password. SMTP password or app password.imap_host — type: string — required. IMAP server hostname, e.g. imap.gmail.com.imap_port — type: integer — optional — default: 993 — range: 1–65535. IMAP port. One of 143, 993. Defaults to 993.imap_username — type: string — required. IMAP login (usually the email address).imap_password — type: string — required — format: password. IMAP password or app password.external_id — type: string — optional — range: –255. Your own reference id for this mailbox (unique per workspace).display_name — type: string — optional — range: –255. Display name shown on outgoing mail.sending_enabled — type: boolean — optional — default: true. Whether the mailbox may send. Defaults to true.daily_send_limit — type: integer — optional — default: 50 — range: 1–1000. Max sends per day. Defaults to 50.auto_enable_warmup — type: boolean — optional — default: false. Enable warmup automatically once the connection test passes.{
"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
}
{
"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"
}
}
validation_error — Body failed validation.mailbox_limit_exceeded — At or above your plan's mailbox limit.duplicate_email — An active mailbox already uses this email.duplicate_external_id — The external_id is already in use.ssrf_blocked — A host resolved to a private/internal IP.invalid_port — A port outside the allowed list (25/465/587/143/993).POST /api/v1/mailboxes/test
Synchronously tests SMTP + IMAP credentials WITHOUT creating a mailbox. Rate limited.
mailboxes:writeemail_address — type: string — required — format: email. The mailbox email address.smtp_host — type: string — required. SMTP server hostname, e.g. smtp.gmail.com.smtp_port — type: integer — optional — default: 587 — range: 1–65535. SMTP port. One of 25, 465, 587. Defaults to 587.smtp_username — type: string — required. SMTP login (usually the email address).smtp_password — type: string — required — format: password. SMTP password or app password.imap_host — type: string — required. IMAP server hostname, e.g. imap.gmail.com.imap_port — type: integer — optional — default: 993 — range: 1–65535. IMAP port. One of 143, 993. Defaults to 993.imap_username — type: string — required. IMAP login (usually the email address).imap_password — type: string — required — format: password. IMAP password or app password.{
"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"
}
{
"data": {
"smtp_ok": true,
"imap_ok": true
},
"meta": {
"request_id": "req_mq28denhy91fh6"
}
}
validation_error — Body failed validation.ssrf_blocked — A host resolved to a private/internal IP.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.
mailboxes:writeid — in: path — type: string — required. Mailbox id (UUID).display_name — type: string — optional — range: –255. Display name.smtp_host — type: string — optional. SMTP host.smtp_port — type: integer — optional — range: 1–65535. SMTP port (25/465/587).smtp_username — type: string — optional. SMTP login.smtp_password — type: string — optional — format: password. SMTP password.imap_host — type: string — optional. IMAP host.imap_port — type: integer — optional — range: 1–65535. IMAP port (143/993).imap_username — type: string — optional. IMAP login.imap_password — type: string — optional — format: password. IMAP password.sending_enabled — type: boolean — optional. Allow sending.daily_send_limit — type: integer — optional — range: 1–1000. Daily send cap.external_id — type: string — optional — nullable — range: –255. Your reference id, or null to clear.signature — type: string — optional — nullable — range: –65536. HTML/text signature, or null to clear.{
"display_name": "Jordan Lee",
"daily_send_limit": 80
}
{
"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"
}
}
validation_error — Body failed validation.not_found — Mailbox not found.duplicate_external_id — The new external_id is already in use.ssrf_blocked — A host resolved to a private/internal IP.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.
mailboxes:writeid — in: path — type: string — required. Mailbox id (UUID).{
"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"
}
}
not_found — Mailbox not found (or already deleted).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.
mailboxes:writeid — in: path — type: string — required. Mailbox id (UUID).{
"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"
}
}
not_found — No soft-deleted mailbox with that id.duplicate_email — An active mailbox already uses this email.POST /api/v1/mailboxes/{id}/test
Re-runs the SMTP + IMAP connection test against the stored credentials and updates the mailbox status. Rate limited.
mailboxes:writeid — in: path — type: string — required. Mailbox id (UUID).{
"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"
}
}
not_found — Mailbox not found.ssrf_blocked — A host resolved to a private/internal IP.GET /api/v1/warmup/stats
Per-mailbox warmup health: progress, today's volume, inbox/spam rate, reputation. Cursor paginated.
warmup:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 50 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.mailbox_id — in: query — type: string — optional. One mailbox id, or comma-separated ids (up to 100), to filter.{
"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 /api/v1/warmup/stats/{mailboxId}
Warmup health for a single mailbox.
warmup:readmailboxId — in: path — type: string — required. Mailbox id (UUID).{
"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"
}
}
not_found — Mailbox not found.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".
warmup:writemailboxId — in: path — type: string — required. Mailbox id (UUID).warmup_enabled — type: boolean — optional. Turn warmup on or off. Enabling requires the mailbox status to be "connected".warmup_type — type: string — optional — one of: new_domain, new_mailbox, dormant, maintenance, recovery, aggressive, normal, conservative. Warmup strategy. Controls ramp speed and target.warmup_target_daily_volume — type: integer — optional — range: 1–100. Target warmup emails per day once fully ramped.{
"warmup_enabled": true,
"warmup_type": "normal",
"warmup_target_daily_volume": 50
}
{
"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"
}
}
validation_error — Body failed validation.not_found — Mailbox not found.mailbox_not_connected — Tried to enable warmup on a mailbox that is not connected.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.
warmup:writemailbox_ids — type: string[] — required — range: 1–500. Mailbox ids to update (1–500).updates — type: object — required. A warmup settings object — same fields as Update warmup settings (warmup_enabled, warmup_type, warmup_target_daily_volume). At least one field required.{
"mailbox_ids": [
"5bd50e20-6b75-4429-9a5a-4a09b46e945a",
"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
],
"updates": {
"warmup_enabled": true,
"warmup_type": "normal"
}
}
{
"data": {
"job_id": "job_7d2f1a",
"status": "processing",
"total_items": 2
},
"meta": {
"request_id": "req_mq28denhy91fh6",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}
}
validation_error — Body failed validation or no update fields given.mailboxes_not_found — One or more mailbox ids don't exist (details.missing_mailbox_ids).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.
jobs:readid — in: path — type: string — required. Job id (UUID).{
"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"
}
}
not_found — Job not found.GET /api/v1/prospects
Returns prospects in your workspace with cursor pagination.
prospects:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 50 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.filter — in: query — type: string — optional. Repeatable metadata filter, formatted field:op:value (e.g. company:eq:Acme, custom:region:eq:US, email:exists). Fields: email, firstName, lastName, company, role, country, city, industry, globalStatus, tags, or custom:<key>. Ops: eq, neq, contains, exists. Unknown/invalid filters are ignored and echoed back in filters_ignored.{
"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",
"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"
}
}
POST /api/v1/prospects
Creates a prospect. Provide at least one of email, linkedinUrl, or instagramUsername — a prospect with no email address is still created and can be enrolled in LinkedIn or Instagram 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.
prospects:writeemail — type: string — optional — format: email. Prospect email (unique per workspace). Optional — provide email, linkedinUrl, or instagramUsername.firstName — type: string — optional. First name.lastName — type: string — optional. Last name.company — type: string — optional. Company.role — type: string — optional. Job title / role.phone — type: string — optional. Phone number.linkedinUrl — type: string — optional — format: uri. LinkedIn profile URL. Use for a LinkedIn-only prospect with no email.instagramUsername — type: string — optional. Instagram handle (with or without the leading @, or a full instagram.com profile URL). Use for an Instagram-only prospect with no email.customFields — type: object — optional. Arbitrary key/value pairs for personalization.listId — type: string — optional. Add the created/updated prospect to this list. When a running campaign is linked to the list, the prospect is auto-enrolled into it (continuous enrollment). Find the list ID on the Lists page.enroll — type: boolean — optional — default: true. When listId is set and a running campaign is linked to that list, auto-enroll the prospect into it. Set false to add to the list only, without enrolling. Defaults to true. Ignored when listId is omitted.enrollInCampaignId — type: string — optional. Enroll the created/updated prospect into this specific campaign in the same call (bypasses lists). Requires the enrollments:write scope. Suppressed prospects are skipped.{
"email": "[email protected]",
"firstName": "Sam",
"listId": "list_9f3a2b",
"enroll": true
}
{
"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",
"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
}
}
validation_error — Body failed validation, or none of email, linkedinUrl, or instagramUsername was provided.prospect_limit_reached — Workspace prospect limit reached.list_not_found — listId did not match a list in your workspace. The prospect is still saved; see list.error in the response body.PATCH /api/v1/prospects/{id}
Partial update of a prospect. linkedinUrl and instagramUsername may be set or cleared (null). globalStatus can be changed (e.g. to mark unsubscribed).
prospects:writeid — in: path — type: string — required. Prospect id (UUID).firstName — type: string — optional. First name.lastName — type: string — optional. Last name.company — type: string — optional. Company.role — type: string — optional. Role.phone — type: string — optional. Phone.linkedinUrl — type: string — optional — format: uri — nullable. LinkedIn URL, or null to clear.instagramUsername — type: string — optional — nullable. Instagram handle (with or without the leading @, or a full instagram.com profile URL), or null to clear.customFields — type: object — optional. Custom fields (replaces existing).globalStatus — type: string — optional — one of: active, bounced, invalid, unsubscribed, dnc, suppressed. Lifecycle status.{
"role": "VP Engineering",
"globalStatus": "active"
}
{
"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",
"customFields": {
"plan": "enterprise"
},
"createdAt": "2026-06-09T14:02:00.000Z",
"updatedAt": "2026-06-09T14:02:00.000Z"
}
}
validation_error — Body failed validation.not_found — Prospect not found.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.
suppressions:writeemails — type: string[] — required — range: 1–1000. Emails to suppress (1–1000).reason — type: string — optional. Optional reason stored with each entry.{
"emails": [
"[email protected]",
"[email protected]"
],
"reason": "manual opt-out"
}
{
"data": {
"suppressedCount": 2,
"alreadySuppressedCount": 0,
"notFoundCount": 0,
"totalProcessed": 2
}
}
validation_error — Body failed validation.GET /api/v1/suppressions
Returns suppressed emails and domains (the blocklist) with cursor pagination.
suppressions:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 50 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.type — in: query — type: string — optional — one of: email, domain. Filter by entry type.{
"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"
}
}
GET /api/v1/campaigns
Returns campaigns with summary stats (sent/opened/replied/...). Cursor paginated.
campaigns:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 50 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.status — in: query — type: string — optional — one of: draft, scheduled, running, paused, completed, error. Filter by status.{
"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 /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).
campaigns:readid — in: path — type: string — required. Campaign id (UUID).{
"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
},
{
"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,
"sentCount": 0,
"openCount": 0,
"clickCount": 0,
"replyCount": 0,
"bounceCount": 0,
"conditionType": "email_opened",
"conditionTrueNext": 2,
"conditionFalseNext": 3,
"conditionTimeoutDays": 3,
"conditions": null
}
]
},
"meta": {
"request_id": "req_mq28denhy91fh6"
}
}
not_found — Campaign not found.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. `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.
campaigns:readid — in: path — type: string — required. Campaign id (UUID).{
"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"
}
}
not_found — Campaign not found.not_linkedin_campaign — The campaign does not send on LinkedIn, so there is no LinkedIn allowance to report.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.
campaigns:writename — type: string — required — range: 1–200. Campaign name.description — type: string — optional — range: –2000. Optional description.channel — type: string — optional — one of: email, linkedin, multichannel — default: email. Campaign channel.linkedinAccountId — type: string — optional — format: uuid. LinkedIn account id (for linkedin/multichannel).timezone — type: string — optional — default: UTC — range: –100. IANA timezone for the sending schedule.dailySendLimit — type: integer — optional — default: 50 — range: 1–10000. Daily send cap for the campaign.sendingWindowStart — type: integer — optional — range: 0–23. Start hour (0–23) of the daily sending window.sendingWindowEnd — type: integer — optional — range: 0–23. End hour (0–23) of the daily sending window.scheduleDays — type: integer[] — optional. Days of week to send (0=Sun … 6=Sat).stopOnReply — type: boolean — optional — default: true. Stop a prospect's sequence when they reply.stopOnBounce — type: boolean — optional — default: true. Stop on bounce.stopOnUnsubscribe — type: boolean — optional — default: true. Stop on unsubscribe.trackOpens — type: boolean — optional — default: true. Track opens.trackClicks — type: boolean — optional — default: true. Track clicks.startDate — type: string — optional — format: date-time. Optional scheduled start.endDate — type: string — optional — format: date-time. Optional end date.steps — type: object[] — required — range: 1–50. Ordered steps. Each: { stepIndex, type (email|linkedin|condition), subject, bodyHtml, bodyText, delayDays, delayHours, isActive, … }. Condition steps additionally take the four condition fields below.steps[].conditionType — type: string — optional — one of: email_opened, email_replied, email_clicked, email_bounced, linkedin_accepted, linkedin_replied, has_linkedin_profile, timeout. Required on a step of type "condition": the signal the branch evaluates. Any other value is rejected with 400 validation_error.steps[].conditionTrueNext — type: integer — optional — range: -1–. Required on a condition step: stepIndex to jump to when the condition is met. Use -1 to end the sequence on the YES branch. Must point forward.steps[].conditionFalseNext — type: integer — optional — nullable — range: -1–. stepIndex to jump to when the condition is NOT met. Omit (or use -1) to end the sequence on the NO branch — a first-class outcome, not an error. Must point forward.steps[].conditionTimeoutDays — type: integer — optional — default: 3 — range: 1–30. How long the condition waits before the NO branch is taken. Defaults to 3 days when omitted.mailboxIds — type: string[] — required — range: –100. Mailbox ids that send this campaign.prospectIds — type: string[] — optional — range: –10000. Prospect ids to enroll on creation.{
"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"
]
}
{
"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
}
}
validation_error — Body / step validation failed — including a conditionType outside the allowed list.unsupported_field — A step supplied "conditions" (multi-condition AND/OR logic). Author those in the campaign builder; the API returns them read-only.campaign_step_validation_error — A step failed sequence-integrity checks (e.g. a branch pointing backward or at a step that does not exist).subscription_blocked — Your plan doesn't allow this action.PATCH /api/v1/campaigns/{id}
Updates campaign settings. Only allowed while the campaign is in draft or paused status.
campaigns:writeid — in: path — type: string — required. Campaign id (UUID).name — type: string — optional — range: 1–200. Campaign name.description — type: string — optional — range: –2000. Description.dailySendLimit — type: integer — optional — range: 1–10000. Daily send cap.sendingWindowStart — type: integer — optional — range: 0–23. Window start hour.sendingWindowEnd — type: integer — optional — range: 0–23. Window end hour.scheduleDays — type: integer[] — optional. Days of week (0–6).stopOnReply — type: boolean — optional. Stop on reply.stopOnBounce — type: boolean — optional. Stop on bounce.stopOnUnsubscribe — type: boolean — optional. Stop on unsubscribe.trackOpens — type: boolean — optional. Track opens.trackClicks — type: boolean — optional. Track clicks.startDate — type: string — optional — format: date-time — nullable. Scheduled start, or null.endDate — type: string — optional — format: date-time — nullable. End date, or null.{
"dailySendLimit": 80,
"scheduleDays": [
1,
2,
3,
4,
5
]
}
{
"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"
}
}
invalid_status — Campaign isn't in draft/paused status.not_found — Campaign not found.POST /api/v1/campaigns/{id}/start
Moves a draft/paused campaign into running (or scheduled, when startDate is in the future). 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.
campaigns:writeid — in: path — type: string — required. Campaign id (UUID).force — in: query — type: string — optional — one of: 1, true. Launch even though the configuration check found problems — the headless equivalent of "Launch anyway" in the app. Omit unless you have reviewed the reported violations.{
"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"
}
}
invalid_status — Campaign can't be started from its current status.campaign_validation_failed — The sequence has configuration problems that would strand prospects. details.violations lists each one with its step index and a fix hint. Resolve them, or retry with ?force=1.subscription_blocked — Your plan doesn't allow this action.not_found — Campaign not found.POST /api/v1/campaigns/{id}/pause
Pauses a running campaign. No request body.
campaigns:writeid — in: path — type: string — required. Campaign id (UUID).{
"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"
}
}
invalid_status — Campaign isn't running.not_found — Campaign not found.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.
enrollments:writeid — in: path — type: string — required. Campaign id (UUID).prospectIds — type: string[] — optional. Prospect ids to enroll.listIds — type: string[] — optional. Audience list ids — every prospect on each list is enrolled.emails — type: string[] — optional. Prospect emails to enroll.{
"emails": [
"[email protected]"
],
"listIds": [
"list_9f3a2b"
]
}
{
"data": {
"enrolledCount": 12,
"suppressedCount": 1,
"invalidCount": 0,
"alreadyEnrolledCount": 2,
"totalProcessed": 15
}
}
validation_error — No targets given or body invalid.not_found — Campaign not found.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.
enrollments:writeid — in: path — type: string — required. Campaign id (UUID).prospectIds — type: string[] — optional. Prospect ids to unenroll.emails — type: string[] — optional. Prospect emails to unenroll.{
"emails": [
"[email protected]"
]
}
{
"data": {
"unenrolledCount": 1,
"totalProcessed": 1
}
}
validation_error — No targets given or body invalid.not_found — Campaign not found.GET /api/v1/webhooks
Returns your configured webhook endpoints (without secrets).
webhooks:read{
"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"
}
]
}
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.
webhooks:writeurl — type: string — required — format: uri — range: –500. HTTPS URL that receives POST deliveries.events — type: string[] — required — range: 1–. Event types to subscribe to (see Webhook Events).{
"url": "https://example.com/hooks/warmysender",
"events": [
"reply.received",
"email.bounced",
"linkedin.reply_received"
]
}
{
"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."
}
validation_error — Invalid URL or unknown event type.PATCH /api/v1/webhooks/{id}
Change a webhook's URL, subscribed events, or active flag.
webhooks:writeid — in: path — type: string — required. Webhook id.url — type: string — optional — format: uri — range: –500. New delivery URL.events — type: string[] — optional — range: 1–. Replacement list of event types.isActive — type: boolean — optional. Enable or disable deliveries.{
"isActive": false
}
{
"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"
}
}
validation_error — Body failed validation.not_found — Webhook not found.DELETE /api/v1/webhooks/{id}
Permanently removes a webhook endpoint. Returns 204 with no body.
webhooks:writeid — in: path — type: string — required. Webhook id.No response body.
not_found — Webhook not found.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.
webhooks:writeid — in: path — type: string — required. Webhook id.{
"data": {
"eventId": "evt_a1b2c3",
"status": "queued",
"message": "Test webhook event has been queued for delivery"
}
}
not_found — Webhook not found.GET /api/v1/linkedin/accounts
Returns connected LinkedIn accounts with their daily engagement limits and how many actions remain today.
linkedin:readlimit — in: query — type: integer — optional — default: 50 — range: 1–100. Results to return (1–100).{
"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
}
}
]
}
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.
linkedin:writepostId — in: path — type: string — required. LinkedIn post id (URN).linkedin_account_id — type: string — required. Which connected LinkedIn account reacts.reaction_type — type: string — optional — one of: LIKE, CELEBRATE, SUPPORT, LOVE, INSIGHTFUL, FUNNY — default: LIKE. Reaction (case-insensitive; defaults to LIKE).{
"linkedin_account_id": "li_5a2b1c",
"reaction_type": "LIKE"
}
{
"data": {
"post_id": "urn:li:activity:7300000000000000000",
"reaction_type": "LIKE",
"linkedin_account_id": "li_5a2b1c"
}
}
account_not_connected — The LinkedIn account isn't connected.account_unauthorized — The LinkedIn session needs re-authentication.daily_limit_reached — The account hit its daily engagement limit.GET /api/v1/linkedin/posts/{postId}/reactions
Lists who reacted to a post, as seen by one of your connected accounts. Cursor paginated.
linkedin:readpagination.next_cursor.postId — in: path — type: string — required. LinkedIn post id (URN).linkedin_account_id — in: query — type: string — required. Which connected account performs the read.limit — in: query — type: integer — optional — default: 20 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.{
"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
}
}
account_not_connected — The LinkedIn account isn't connected.account_unauthorized — The LinkedIn session needs re-authentication.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.
verification:writeemail — type: string — required — format: email. The address to check.{
"email": "[email protected]"
}
{
"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"
}
}
verifier_access_required — The subscription is inactive.verifier_access_required — The current plan does not include email verification.verification_rate_limited — More than 20 single checks in a minute for this workspace.allowance_exhausted — The plan allowance is used up and there are no credits to cover the check. Nothing was charged.verification_unavailable — Checking is temporarily unavailable. Nothing was charged.POST /api/v1/verification/batches
Submits a list of addresses to be checked in the background and returns a run id to poll. If your allowance covers only part of the list, the covered part starts immediately and the rest is reported back as deferred — nothing is charged for it.
verification:writeemails — type: string[] — required — range: 1–20000. Addresses to check (1–20,000). Duplicates are collapsed.name — type: string — optional. Label for this run. Defaults to an auto-generated one.{
"emails": [
"[email protected]",
"[email protected]"
],
"name": "Q3 outbound list"
}
{
"data": {
"id": "b1a2c3d4-...",
"status": "processing",
"accepted": 2,
"submitted": 2
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
batch_too_large — More than 20,000 addresses were submitted in one run.verifier_access_required — The subscription is inactive.verifier_access_required — The current plan does not include email verification.verification_rate_limited — More than 5 runs started in a minute for this workspace.allowance_exhausted — The allowance and credit balance cover none of the list. Nothing was charged.GET /api/v1/verification/batches
Lists this workspace's verification runs, newest first, with their progress and result counts.
verification:readpagination.next_cursor.limit — in: query — type: integer — optional — default: 25 — range: 1–100. Results per page (1–100).cursor — in: query — type: string — optional. next_cursor from the previous page.{
"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"
}
],
"pagination": {
"has_more": false
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
GET /api/v1/verification/batches/{id}
Returns the progress and result counts for one verification run.
verification:readid — in: path — type: string — required. The run id.{
"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
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
not_found — No run with that id in this workspace.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.
verification:readid — in: path — type: string — required. The run id.status — in: query — type: string — optional — one of: pending, processing, valid, invalid, risky, unknown, error, cancelled. Return only addresses with this outcome. Omit for all.limit — in: query — type: integer — optional — default: 100 — range: 1–1000. Results per page (1–1,000).offset — in: query — type: integer — optional — default: 0 — range: 0–. Result offset for paging.{
"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"
}
}
validation_error — The `status` filter is not one of the listed outcomes.not_found — No run with that id in this workspace.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.
verification:writeid — in: path — type: string — required. The run id.{
"data": {
"id": "b1a2c3d4-...",
"status": "cancelled",
"cancelled": true,
"stopped_addresses": 1360,
"credits_refunded": 1360,
"credit_balance": 4360
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
not_found — No run with that id in this workspace.POST /api/v1/verification/batches/{id}/create-list
Builds a reusable prospect list from a finished run. It keeps two kinds of address: ones whose mailbox was confirmed, and ones on a mail server that accepts every address, where no tool can confirm that specific mailbox. Both are kept on purpose — many of the unconfirmable ones deliver perfectly well — and the response reports the split so you can decide how to send.
verification:writeid — in: path — type: string — required. The run id.{
"data": {
"created": true,
"list_id": "l9f8e7d6-...",
"list_name": "Q3 outbound list (verified)",
"address_count": 1712,
"confirmed_count": 604,
"unconfirmable_count": 1108
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
not_found — No run with that id in this workspace.batch_not_finished — The run is still going, or it did not finish successfully.GET /api/v1/verification/allowance
Returns how many addresses can be checked right now — the plan allowance left this month and today, the purchased credit balance, and the largest run accepted. Read this before submitting so you never discover a limit by being refused.
verification:read{
"data": {
"monthly_allowance": 10000,
"monthly_used": 1240,
"monthly_remaining": 8760,
"daily_allowance": 333,
"daily_used": 40,
"daily_remaining": 293,
"credit_balance": 5000,
"verifiable_now": 5293,
"max_addresses_per_batch": 20000
},
"meta": {
"request_id": "req_lz3k9a2b"
}
}
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.receivedA 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.bouncedAn 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.unsubscribedA prospect unsubscribed.
{
"prospectId": "f3c4a2d1-...",
"prospectEmail": "[email protected]",
"campaignId": "c1a2b3c4-...",
"unsubscribedAt": "2026-06-10T11:02:00.000Z"
}
email.openedA tracked email was opened.
{
"prospectId": "f3c4a2d1-...",
"prospectEmail": "[email protected]",
"campaignId": "c1a2b3c4-...",
"stepIndex": 0,
"openedAt": "2026-06-10T11:03:00.000Z"
}
email.clickedA tracked link was clicked.
{
"prospectId": "f3c4a2d1-...",
"prospectEmail": "[email protected]",
"campaignId": "c1a2b3c4-...",
"url": "https://yourdomain.com/demo",
"clickedAt": "2026-06-10T11:04:00.000Z"
}
prospect.suppressedA prospect was added to the suppression list.
{
"prospectId": "f3c4a2d1-...",
"email": "[email protected]",
"reason": "manual opt-out",
"suppressedAt": "2026-06-10T11:05:00.000Z"
}
limit.hitA 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_acceptedA 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_receivedA 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.testA 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"
}
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).
missing_idempotency_key — An Idempotency-Key header is required for this request.validation_error — The request body or parameters failed validation.unauthorized — Missing or invalid API key.insufficient_scope — The API key lacks the scope this endpoint requires.not_found — The resource does not exist (or you have no access to it).idempotency_conflict — The Idempotency-Key was reused with a different request body.ssrf_blocked — A supplied host resolved to a private/internal IP and was blocked.rate_limited — Too many requests — slow down and retry after the Retry-After delay.internal_error — Something went wrong on our side.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.
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.