Campaigns

A campaign is one email, one audience, one departure. Everything before the send is reversible; the send itself never is.

What a campaign is, over HTTP

A campaign is a template, a subject and a target: one list, optionally narrowed to one tag. It also picks who signs it — one of the addresses in your sender address book, or, by default, the organization's default address.

Scheduling: one optional field, same endpoint

FieldInRequiredDescription
scheduledAtbodynoISO 8601 instant with an explicit offset. Omitted, the campaign goes now
No body at all, an empty body, or a body without scheduledAt all mean the same thing: send now. Callers written before this field have nothing to change.
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
  -d '{"scheduledAt": "2026-08-12T07:00:00Z"}' \
  "https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID/send"
json
{"success": true, "data": {"campaignId": "…", "status": "scheduled"}}
status tells the two apart: send_requested for a send starting now, scheduled for one booked for later. The campaign moves to scheduled, and GET /api/v1/campaigns/{campaignId} gives back the instant that was actually stored — worth reading once, since the schedule is kept to the minute and the seconds you pass are dropped.
A date with no timezone is refused, never read as UTC. 2026-08-12T09:00:00 answers 400: the API does not guess an offset you did not write, because guessing it wrong sends an hour or two off with nothing in the request to show for it. Write 2026-08-12T07:00:00Z, or 2026-08-12T09:00:00+02:00 — both name the same instant. An instant in the past, or less than a couple of minutes away, is refused the same way.
Scheduling is for a draft, and there is no API way back. A campaign that is already scheduled answers 400 — it is not rescheduled in place. Cancelling a schedule is done from the app, on the campaign screen; no endpoint exposes it. Book the time you mean.
Campaign statePOST /sendwith scheduledAt
draft202 — the send is requested202 — the campaign is scheduled
scheduled202 — it goes now400 — only a draft can be scheduled
sending409 — a send is already running409
sent409 — it already went out409
failed409 — not in a sendable state409
no unsubscribe link in the content400 — nothing is emitted400 — nothing is scheduled
unknown, or another organization404404
The content is rendered before anything is emitted. It is the exact same check as POST /api/v1/render: a campaign whose HTML carries no {{unsubscribeUrl}} answers 400 and no send is created. Without it, every single send would fail later for a reason the API never shows you. Fix the template, recreate the campaign, then send.
Calling send twice never sends twice. A campaign that already went out answers 409 and no second send is started — retrying after a timeout is safe. If you want to send the same content again, create a new campaign.
Then poll GET /api/v1/campaigns/{campaignId}: status moves draftsendingsent, and progress fills up along the way. There is no webhook to wait for.

When polling never converges

A 202 means the request was accepted, not that the send will happen. Two conditions stop it after the fact, and neither changes the campaign: it stays draft, progress stays at zero, and no error is ever reported to you.
What you seeWhat it meansWhat to do
draft, progress.total = 0, sendingEnabled = falseMass sending is switched off account-wide (kill switch)Nothing on your side — sending has to be re-enabled
draft, progress.total = 0, sendingEnabled = trueThe 24 h sending quota would be exceeded by this campaign's recipientCountWait for the quota window to roll, then call /send again
Stop polling if the campaign is still draft with no progress. It will never move to sent on its own. Read sendingEnabled: false means the kill switch, true means the daily quota — in both cases the campaign is intact and calling /send again later is safe, it is still a draft.

A campaign from nothing, end to end

1

Write the template

bash
TEMPLATE_ID=$(curl -s -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "August newsletter",
    "defaultSubject": "What shipped in August",
    "content": "<html><body><p>Hi {{firstName}}</p><p>Here is what we shipped.</p><a href=\"{{unsubscribeUrl}}\">Unsubscribe</a></body></html>"
  }' \
  "https://www.agentsmail.io/api/v1/templates" | jq -r '.data.id')
Check unsupportedTags in the response: anything listed there is still raw in your HTML.
2

Render it, and read what you get

bash
curl -s -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
  -d '{"templateId":"'$TEMPLATE_ID'","variables":{"firstName":"Camille"}}' \
  "https://www.agentsmail.io/api/v1/render" | jq '{subject, unknownVariables: .data.unknownVariables}'
A 400 here means the HTML has no unsubscribe link. Fix the template with PATCH and render again — this loop costs nothing.
3

Pick the target

bash
LIST_ID=$(curl -s -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/lists" | jq -r '.data[0].id')

curl -s -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/tags" | jq '.data[] | {id, name, contactCount}'
List ids come from GET /api/v1/lists — see Discovery. Tags come from the Audience API; keep the id of the one you want, if any.
4

Create the campaign

bash
CAMPAIGN_ID=$(curl -s -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "August newsletter",
    "subject": "What shipped in August",
    "templateId": "'$TEMPLATE_ID'",
    "listId": "'$LIST_ID'"
  }' \
  "https://www.agentsmail.io/api/v1/campaigns" | jq -r '.data.id')
5

Count the recipients before committing

bash
curl -s -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID" | jq '.data.recipientCount'
Zero means the target matches nobody. Sending would be a no-op — fix the target first.
6

Send

bash
curl -s -X POST -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID/send"
202 means the send was accepted, not finished.
7

Follow it

bash
curl -s -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID" | jq '{status: .data.status, progress: .data.progress}'
Poll until status is sent. Do not call /send again while waiting — it answers 409. If the campaign is still draft with progress.total at zero, stop polling and read When polling never converges above: sending is switched off, or the daily quota is full.