API Documentation & Integrations

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

OpenAPI specification

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

Authentication

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

Authorization: Bearer your-api-key-here

Base URL

All API endpoints are available at:

https://warmysender.com/api/v1

Rate Limiting

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

Scopes

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

API reference

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

Identity

Get the authenticated API key + workspace

GET /api/v1/me

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

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

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

Create a prospect

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.

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

Update a prospect

PATCH /api/v1/prospects/{id}

Partial update of a prospect. linkedinUrl and instagramUsername 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",
    "instagramUsername": "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. Steps of type "condition" additionally carry conditionType, conditionTrueNext, conditionFalseNext, conditionTimeoutDays and conditions (read-only).

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

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

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

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

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

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": "condition",
      "name": "Opened the intro?",
      "conditionType": "email_opened",
      "conditionTrueNext": 2,
      "conditionFalseNext": 3,
      "conditionTimeoutDays": 3,
      "delayDays": 2
    },
    {
      "stepIndex": 2,
      "type": "email",
      "subject": "Re: Quick question",
      "bodyHtml": "<p>Following up ...</p>",
      "delayDays": 3,
      "sendAsReply": true
    },
    {
      "stepIndex": 3,
      "type": "email",
      "subject": "One more idea, {{firstName}}",
      "bodyHtml": "<p>Different angle ...</p>",
      "delayDays": 3
    }
  ],
  "prospectIds": [
    "f3c4a2d1-9b7e-4c2a-8f10-2b6d5e8a1c44"
  ]
}
Response — 201
{
  "data": {
    "id": "c1a2b3c4-d5e6-47f8-9a0b-1c2d3e4f5a6b",
    "name": "Q3 Outbound — Founders",
    "status": "draft",
    "channel": "email",
    "description": null,
    "timezone": "UTC",
    "dailySendLimit": 50,
    "sendingWindowStart": null,
    "sendingWindowEnd": null,
    "scheduleDays": null,
    "stopOnReply": true,
    "stopOnBounce": true,
    "stopOnUnsubscribe": true,
    "trackOpens": true,
    "trackClicks": true,
    "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 (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.

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

Verification

Verify one email address

POST /api/v1/verification/verify

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

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

Start a verification run

POST /api/v1/verification/batches

Submits a list of addresses to be checked in the background and returns a run id to poll. 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.

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

List verification runs

GET /api/v1/verification/batches

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

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

Get a verification run

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

Returns the progress and result counts for one verification run.

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

Get per-address results

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

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

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

Stop a run and refund unused credits

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

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

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

Build a prospect list from results

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

Builds a reusable prospect list from a finished run. 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.

Parameters
Response — 201
{
  "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"
  }
}
Errors

Check remaining verification allowance

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.

Response — 200
{
  "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"
  }
}

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