Draft a campaign to a whole segment. This endpoint does not send — it hands the draft to a human, who approves or discards it in the app. It is the one endpoint built for software that writes marketing email, and the approval step is why we are comfortable offering it.
| Method | Path | Description |
|---|---|---|
POST | /api/v1/send/bulk | Draft a campaign to a segment and hold it for approval. |
Requires the email:send_bulk scope. A transactional email:send key is rejected here with 403 insufficient_scope, on purpose: a key that sends receipts should not be able to mail your whole list. The scope is never granted to an OAuth-connected agent and never inherited by default — see Scopes.
A successful call returns pending_approval. At that moment no recipient has been queued and no email has been sent. The workspace owner sees the draft in the app with the segment, the recipient count, a rendered preview and the deliverability verdict, and chooses to send, edit or cancel it.
The approval cannot come from the API. Approving is a signed-in action in the dashboard, so the software that drafted the campaign cannot approve its own work — and if nobody ever approves it, nothing is ever sent. Editing the draft in the app invalidates the earlier approval, so what goes out is always what someone actually looked at.
ps_test_ key can draft a bulk send, and approving it delivers for real. To rehearse content, send yourself a normal transactional message first.| Field | Type | Required | Description |
|---|---|---|---|
segment_id | string (uuid) | required | An existing audience segment in your workspace. Create one with POST /segments, or build it in the app. An unknown or foreign id returns 404. |
subject | string | required | Subject line. The approver sees this before deciding. |
html | string | required | HTML body. The campaign footer and one-click unsubscribe are added automatically — you don't supply them. |
| Field | Type | Description |
|---|---|---|
status | "pending_approval" | Always this value. It is the only outcome this endpoint has. |
approval_id | string (uuid) | The held campaign's id. It appears in the workspace owner's approvals queue. |
recipient_count | number | Subscribed, non-suppressed contacts the segment resolved to at draft time. The reviewed set is frozen here, so approving sends to exactly these people. |
deliverability | object | null | The content verdict shown to the approver, in the same shape as the deliverability check. null when the check was unavailable — which does not block the draft. |
The campaign uses your workspace default sender, and suppression, the footer and one-click unsubscribe are applied at send time — the same path a campaign built in the app takes.
There is no callback when someone approves. Poll GET /events or GET /emails — once the campaign is approved its sends appear there like any other, with delivery and engagement events following. A draft nobody approves simply produces nothing, which is the intended behaviour rather than a failure to handle.
| Status | code | When |
|---|---|---|
400 | missing_field | segment_id, subject or html absent. |
400 | invalid_field | A field is present but the wrong type, segment_id is not a UUID, or the segment currently resolves to no subscribed recipients (param: segment_id). |
401 | unauthorized | Missing or invalid API key. |
403 | insufficient_scope | The key lacks email:send_bulk. |
403 | account_not_approved | The workspace is not approved for sending yet. |
404 | not_found | No such segment in this workspace. A segment belonging to another workspace returns 404 too — never a hint that it exists. |
429 | rate_limited | Per-key rate limit exceeded. |
See Error codes for the envelope and the full list.