API reference

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
}
  • status is the raw column. display_status is one of draft, running, paused, done, archived or other, and matches the badge in the app. An active campaign at 100% progress with at least one enrolment is done. archived_at takes precedence.
  • progress.planned is the number of enrolled prospects multiplied by the number of email steps. progress.done counts the steps passed by active enrolments plus all steps of stopped and completed enrolments. pct runs from 0 to 100.
  • Campaigns have no updated_at. The timestamps are created_at, started_at and archived_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.

  • delivered is sent minus bounced.
  • replies leaves out autoresponders and reports them separately as autoreplied.
  • queued is the number of active enrolments whose next step is in the future. finished is the number of completed enrolments.
  • invalid counts prospects with an invalid verification result. It is null when nobody in this campaign has been verified.
  • opened and clicked are null until the tracking domain of the workspace is verified, and tracking_enabled is false. null means the value is not measured. It does not mean zero.
  • settings.window_start_hour and settings.window_end_hour can be null, 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
}
  • id is the id of the enrolment. The id of the prospect is in prospect.id.
  • current_step is the number of the step that goes out next. 1 means nothing has gone out yet.
  • outcome.label is Active, Stopped or Completed, the word on the badge in the app. outcome.negative is true when the enrolment stopped because of a bounce, an unsubscribe or a suppression. outcome.reason is 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"] }
  ]
}