Campaigns API

Create email campaigns, point them at a list, schedule them or send them now, and read their results. All routes need a bearer API key (see API Overview).

The campaign object

{
  "id": "clx...",
  "name": "October newsletter",
  "subject": "What's new this month",
  "status": "SENT",
  "listId": "clx...",
  "scheduledAt": null,
  "sentAt": "2026-10-04T14:00:00.000Z",
  "createdAt": "2026-10-03T09:00:00.000Z",
  "stat": { "sent": 1200, "opened": 540, "clicked": 130, "bounced": 4, "unsubscribed": 2 }
}

stat is null until the campaign has stats. stat.unsubscribed is not currently incremented by the unsubscribe flow, so it can read 0 even when people have opted out. Read opt-outs from contacts (status UNSUBSCRIBED) instead.

A campaign created through the API stores the HTML you send in body. It is sent as written, with the unsubscribe footer and tracking added at send time. To design with blocks, use the dashboard builder or the AI connector.

Limits: name up to 200 characters, subject up to 998 characters, body up to 512 KB.

List campaigns

GET /api/v1/campaigns
QueryMeaning
limitPage size, default 20, maximum 100
afterCursor from the previous page
statusDRAFT, SCHEDULED or SENT. Anything else is a 422
qCase-insensitive search in name and subject

Create a campaign

POST /api/v1/campaigns
{
  "name": "October newsletter",
  "subject": "What's new this month",
  "body": "<html>...</html>",
  "listId": "clx..."
}

name, subject and body are required. listId is optional and must be a list in your workspace (404 otherwise). The campaign is created as a DRAFT and nothing is sent.

Get, update and delete

GET    /api/v1/campaigns/:id
PATCH  /api/v1/campaigns/:id
DELETE /api/v1/campaigns/:id

PATCH accepts name, subject, body and listId (use null to clear the list). Each field is validated like on create, and an empty value is rejected. A campaign that is already SENT returns 422 with code CAMPAIGN_ALREADY_SENT. DELETE returns 204.

Schedule a campaign

POST /api/v1/campaigns/:id/schedule
{ "scheduledAt": "2026-10-10T15:00:00Z" }
  • The campaign needs a list first, otherwise 422.
  • scheduledAt is an ISO 8601 date and time, and must be at least 30 seconds in the future.
  • A sent campaign cannot be scheduled (CAMPAIGN_ALREADY_SENT).
  • Returns id, name, status (SCHEDULED) and scheduledAt.

To cancel, call DELETE /api/v1/campaigns/:id/schedule. It only works on a SCHEDULED campaign and sets it back to DRAFT.

Send a campaign now

POST /api/v1/campaigns/:id/send

The body is optional JSON. Two optional fields exist: excludeEmails (an array of addresses to skip) and overrideQuietHours (true to ignore your quiet-hours window).

The send goes through the same guardrails as a dashboard send: suppressed addresses and blocked domains are skipped, and your monthly plan limit is enforced. A send that would pass your daily cap, or that starts inside quiet hours, is refused as a whole with a 422. A scheduled campaign that hits quiet hours is retried after they end. Your workspace also needs a physical mailing address in settings, because it is printed in every footer. See Compliance and Sending Health.

Recipients go out in batches of 100. The request stops starting new batches after about 45 seconds. If recipients are still owed, the rest goes out in the background, picked up by a job that runs every minute:

{ "data": { "sent": 800, "failed": 0, "remaining": 400, "complete": false } }

sent and failed count this call only. When complete is false, do not call again to finish the job: the rest goes out on its own, and a repeated call never mails anyone twice. Poll GET /api/v1/campaigns/:id until status is SENT.

Errors:

HTTPCodeWhen
404NOT_FOUNDThe campaign is not in your workspace
422CAMPAIGN_ALREADY_SENTThe campaign was already sent
403FORBIDDENThe workspace is suspended
402QUOTA_EXCEEDEDThe send would pass your monthly limit
422VALIDATION_ERRORNo audience, no subscribed contacts, or another send blocker