Send or schedule

POST /api/v1/campaigns/{campaignId}/send Triggers the send and answers 202 immediately. The send itself runs in the background, paced against the sending quota — the request never waits for it. Called with no body, it sends now. Called with a scheduledAt, it books the send for that instant — same endpoint, same key, same permissions.
json
{"success": true, "data": {"campaignId": "…", "status": "send_requested"}}

Scheduling: one optional field, same endpoint

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.

Authorization

Role in the organizationThis endpoint
OWNERAllowed
ADMINAllowed
MEMBERRefused — 403
A key carries the role its creator holds in this organization — never a role on AgentsMail itself. See API keys.

Request body

FieldTypeRequiredDescription
scheduledAtstringnoISO 8601 instant with an explicit offset. Omitted, the campaign goes now

Example

bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" \
  "https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID/send"

Response

  • 202 Accepted — the send is queued; follow it on the campaign.
  • 401 Unauthorized — missing or invalid key, or its owner left the organization.
  • 403 Forbidden — the key's owner is a member of the organization, not an admin.
  • 404 Not Found — no such resource, or it belongs to another organization. The API never confirms that an id exists to a caller who has no right to it.
  • 429 Too Many Requests — over 120 requests in a minute for this key.

Response fields

FieldTypeDescription
successbooleanIndicates if the operation was successful
data.statusstringsend_requested when it leaves now, scheduled when a date was given

Example response

json
{
  "success": true,
  "data": {
    "status": "subscribed"
  }
}

Agent recipe

A send is asynchronous: the call returns once the departure is accepted, not once the emails are out. Poll the campaign to follow it — and stop polling when sending is disabled, or the loop never converges.
js
await fetch(`${base}/campaigns/${campaignId}/send`, {method: 'POST', headers})

for (;;) {
  const {data} = await (
    await fetch(`${base}/campaigns/${campaignId}`, {headers})
  ).json()

  if (!data.sendingEnabled) throw new Error('sending is paused: polling would never end')
  if (data.status === 'sent' || data.status === 'failed') return data
  await new Promise((resolve) => setTimeout(resolve, 5000))
}
Never call this without explicit human approval for that specific campaign. Approval for one send never covers the next one.