Campaigns
A campaign is a sequence, a list of prospects and the mailboxes it sends from. The API reads campaigns and controls three actions on an existing campaign: enrol prospects, pause and resume. It does not create campaigns, edit steps or start a draft.
| Method | Path | Action |
|---|---|---|
GET |
/api/v1/campaigns |
List campaigns with progress |
GET |
/api/v1/campaigns/{id} |
Get a campaign with settings, steps and statistics |
GET |
/api/v1/campaigns/{id}/prospects |
List the enrolments of a campaign |
POST |
/api/v1/campaigns/{id}/prospects |
Enrol existing prospects |
POST |
/api/v1/campaigns/{id}/pause |
Pause a running campaign |
POST |
/api/v1/campaigns/{id}/resume |
Resume a paused campaign |
List campaigns#
| Parameter | Type | Notes |
|---|---|---|
status |
string | One of draft, active, paused, completed. This is the raw status column. |
folder_id |
UUID | Campaigns in one folder. |
archived |
string | true or false. |
limit |
integer | 1 to 100. Default 50. |
cursor |
string | See Pagination. |
curl "https://app.anymail.watch/api/v1/campaigns?status=active" \
-H "Authorization: Bearer $ANYMAIL_TOKEN"
Response 200
{
"data": [
{
"id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Q3 outbound",
"status": "active",
"display_status": "running",
"pause_reason": null,
"campaign_type": "outbound",
"folder": { "id": "8d1c0e2a-7b3f-4c5d-9e6a-1f2b3c4d5e6f", "name": "Sales" },
"steps": 4,
"progress": { "planned": 400, "done": 312, "enrolled": 100, "finished": 61, "pct": 78 },
"created_at": "2026-08-10T09:00:00.000+00:00",
"started_at": "2026-08-11T07:30:00.000+00:00",
"archived_at": null
}
],
"next_cursor": null
}
statusis the raw column.display_statusis one ofdraft,running,paused,done,archivedorother, and matches the badge in the app. Anactivecampaign at 100% progress with at least one enrolment isdone.archived_attakes precedence.progress.plannedis the number of enrolled prospects multiplied by the number of email steps.progress.donecounts the steps passed by active enrolments plus all steps of stopped and completed enrolments.pctruns from 0 to 100.- Campaigns have no
updated_at. The timestamps arecreated_at,started_atandarchived_at.
Errors: 400 for an invalid status, folder_id, archived, limit or cursor.
Get a campaign#
GET /api/v1/campaigns/{id} returns the row above plus settings, steps, mailboxes and statistics.
curl "https://app.anymail.watch/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" \
-H "Authorization: Bearer $ANYMAIL_TOKEN"
Response 200 (the fields of the list row are shortened)
{
"id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Q3 outbound",
"status": "active",
"display_status": "running",
"settings": {
"daily_enroll_limit": 20,
"timezone": null,
"use_prospect_timezone": true,
"window_start_hour": 9,
"window_end_hour": 17,
"send_on_weekends": false,
"send_days": null,
"business_days_only": false
},
"step_list": [
{ "id": "a1b2c3d4-0000-4000-8000-000000000001", "position": 1, "delay_days": 0, "subject": "Quick question" }
],
"mailboxes": [
{ "id": "b2c3d4e5-0000-4000-8000-000000000001", "email": "sales@example.com" }
],
"stats": {
"sent": 312,
"delivered": 305,
"bounced": 7,
"replied": 19,
"unsubscribed": 2,
"opened": null,
"clicked": null,
"reply_rate": 0.0609,
"replies": { "total": 19, "interested": 6, "neutral": 9, "notInterested": 4, "autoreplied": 3 },
"prospects": { "enrolled": 100, "queued": 31, "finished": 61, "invalid": null },
"tracking_enabled": false
}
}
The statistics come from the same source as the Stats tab in the app.
deliveredissentminusbounced.repliesleaves out autoresponders and reports them separately asautoreplied.queuedis the number of active enrolments whose next step is in the future.finishedis the number of completed enrolments.invalidcounts prospects with aninvalidverification result. It isnullwhen nobody in this campaign has been verified.openedandclickedarenulluntil the tracking domain of the workspace is verified, andtracking_enabledisfalse.nullmeans the value is not measured. It does not mean zero.settings.window_start_hourandsettings.window_end_hourcan benull, which means they inherit the workspace value.
Errors: 404 with {"error":"Campaign not found."}. This includes a campaign from another workspace and an id that is not a UUID.
List enrolments#
GET /api/v1/campaigns/{id}/prospects returns one row for each person enrolled in the campaign.
| Parameter | Type | Notes |
|---|---|---|
status |
string | One of active, stopped, completed. |
limit |
integer | 1 to 100. Default 50. |
cursor |
string | See Pagination. |
curl "https://app.anymail.watch/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/prospects?status=stopped" \
-H "Authorization: Bearer $ANYMAIL_TOKEN"
Response 200
{
"data": [
{
"id": "e0a1c2d3-0000-4000-8000-000000000001",
"prospect": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": null,
"company_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427"
},
"status": "stopped",
"current_step": 3,
"next_step_at": null,
"outcome": { "label": "Stopped", "negative": true, "reason": "Bounced: 550 5.1.1 user unknown" },
"enrolled_at": "2026-08-11T07:30:00.000+00:00"
}
],
"next_cursor": null
}
idis the id of the enrolment. The id of the prospect is inprospect.id.current_stepis the number of the step that goes out next.1means nothing has gone out yet.outcome.labelisActive,StoppedorCompleted, the word on the badge in the app.outcome.negativeistruewhen the enrolment stopped because of a bounce, an unsubscribe or a suppression.outcome.reasonis the last error recorded on the enrolment.
Errors: 404 with Campaign not found. The API checks the campaign before it reads enrolments. 400 for an invalid status, limit or cursor.
Enrol prospects#
POST /api/v1/campaigns/{id}/prospects enrols existing prospects of the workspace in a draft or active campaign. It runs the same function as Add prospects on the campaign page, so the rules apply to each person separately. The call does not fail as a whole.
Body (JSON, up to 64 KiB). Send exactly one of these fields, with 1 to 200 items. Duplicates in the list are merged and the first one wins. Addresses are compared trimmed and lower-cased.
| Field | Type | Notes |
|---|---|---|
prospect_ids |
array of UUID | Prospect ids. |
emails |
array of string | Email addresses of existing prospects. |
curl -X POST "https://app.anymail.watch/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/prospects" \
-H "Authorization: Bearer $ANYMAIL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emails":["ada@example.com","bob@example.com","nobody@example.com"]}'
Response 200
{
"campaign": { "id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Q3 outbound", "status": "active" },
"data": [
{ "prospect_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "email": "ada@example.com", "result": "enrolled" },
{ "prospect_id": "1b4e28ba-2fa1-11d2-883f-0016d3cca427", "email": null, "result": "already-enrolled" },
{ "prospect_id": null, "email": "nobody@example.com", "result": "not-found" }
],
"summary": {
"requested": 3,
"enrolled": 1,
"already-enrolled": 1,
"suppressed": 0,
"conflict": 0,
"missing-field": 0,
"anonymized": 0,
"not-found": 1
}
}
result |
Meaning |
|---|---|
enrolled |
The prospect entered the campaign. Step 1 is scheduled for now, with a mailbox from the rotation of the campaign. |
already-enrolled |
The prospect is already in this campaign. |
suppressed |
The address is on the suppression list of the workspace or on the global list. |
conflict |
The prospect is active in another campaign of the workspace. |
missing-field |
A snippet used in a subject or body has no value for this prospect. |
anonymized |
The prospect was anonymised after an erasure request, so it cannot be enrolled. |
not-found |
No such prospect in this workspace. This includes the id of another workspace. |
data keeps the order of the request after duplicates are merged. For not-found, prospect_id is null when you sent emails, and email is null when you sent prospect_ids. email is also null for already-enrolled.
The status is 200 even when nobody was enrolled. Read the numbers in summary.
Errors
| Status | Message |
|---|---|
400 |
The body errors listed on Errors, Provide exactly one of `prospect_ids` or `emails`., `prospect_ids` must be a non-empty array of up to 200 UUIDs., `emails` must be a non-empty array of up to 200 email addresses., `prospect_ids[3]` must be a UUID., `emails[2]` must be a non-empty string. |
404 |
Campaign not found. |
409 |
This campaign can't take new prospects in its current state (paused)., Add step 1 before enrolling anyone. or This campaign has no sending mailbox. |
To create a prospect and enrol it in one call, use POST /prospects?campaign=<id>. See Prospects.
Pause a campaign#
POST /api/v1/campaigns/{id}/pause has no body. It does what Pause does in the app: status becomes paused and pause_reason becomes Paused by the operator. Enrolments stay in place. The engine sends nothing from a paused campaign, and a resume continues each sequence where it stopped.
curl -X POST "https://app.anymail.watch/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/pause" \
-H "Authorization: Bearer $ANYMAIL_TOKEN"
Response 200 returns the campaign in the list shape, without the data wrapper, with "status": "paused" and "display_status": "paused".
Errors: 404 with Campaign not found. A 409 explains why the campaign cannot be paused. Only a running campaign can be paused, so a draft, an already paused campaign and a completed campaign each return 409.
Resume a campaign#
POST /api/v1/campaigns/{id}/resume has no body. It follows the Resume campaign path of the app, with the same checks:
- The campaign has a sending mailbox.
- The campaign has at least one step.
- The audience has prospects.
- Every prospect of the audience has all the fields used in the steps and in the active A/B variants.
The write sets status to active, sets started_at to the current time and clears pause_reason. It does not create or undo enrolments.
Only a paused campaign can be resumed. Starting a draft for the first time and running a completed campaign again need decisions in the app, so the API returns 409 for both.
curl -X POST "https://app.anymail.watch/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/resume" \
-H "Authorization: Bearer $ANYMAIL_TOKEN"
Response 200 returns the campaign in the list shape with "status": "active".
Errors
| Status | Message |
|---|---|
404 |
Campaign not found. |
409 |
Campaign is already running. |
409 |
Only a paused campaign can be resumed followed by the reason: the campaign is a draft (start it from the app) or completed (run it again from the app). |
409 |
This campaign has no sending mailbox., Add at least one step first. or This campaign's audience has no prospects. |
409 |
A message that starts with the number of prospects missing a field used in the copy. The campaign stays paused. The body adds an issues array. |
{
"error": "2 prospect(s) are missing a field used in the copy. Fix them or narrow the audience — the campaign stays paused.",
"issues": [
{ "email": "ada@example.com", "missing": ["first_name"] }
]
}