Skip to content
Docs

Nexmail for AI agents.

A REST API for campaigns, built so an AI agent can do the work and a person still decides what goes out. Bearer tokens, JSON in and out, and refusals you can branch on.

Last updated . Checked against the API code on that date.

Rules for agents

Follow these exactly. They are how the API is built, not suggestions.

  1. Use only the endpoints on this page. Do not guess other paths.
  2. Never launch a campaign a person has not approved. The launch call must name that person, and it must echo the approval digest they saw.
  3. Branch on the HTTP status and on code when present. Never on the wording of error; it changes.
  4. A 202 from a launch means queued, not sent. Read the campaign afterwards to see what happened.
  5. Never retry a 422 with a freshly read digest unless a person approved again. That defeats the approval.
  6. Treat a held or deferred recipient as waiting, not failed. Nexmail never drops mail; it waits.
  7. Never print, log or store the token anywhere but a secret store.

Get a credential

  1. An administrator signs in to Nexmail and opens Settings → Credentials.
  2. They create a credential, name it after the agent (for example crm-agent), and tick only the scopes it needs.
  3. Nexmail shows the token once. Store it in a secret manager. A lost token cannot be recovered; mint a new one and revoke the old.

The token looks like mgw_<24 hex characters>_<secret>. Send it on every request:

Authorization: Bearer mgw_0123456789abcdef01234567_REPLACE_WITH_SECRET
Content-Type: application/json

Base URL: https://nexmail.nexprove.com. Every path below is relative to it.

Scopes

ScopeAllows
campaigns:readList and read campaigns, read the approval digest, preview, read recipients and send limits. Required for any launch flow: without it you cannot read the digest the launch must echo.
campaigns:writeCreate, edit and delete draft campaigns; add recipients; mark reply outcomes.
campaigns:sendLaunch a campaign, with a person’s approval attached.
campaigns:prerenderedCreate campaigns whose recipients each carry finished subject, HTML and text. Not implied by campaigns:write. Only for systems that render and approve mail themselves.

A credential belongs to one organization and acts only inside it. It is not a user: it can never reach settings, raise a send limit, lift the emergency stop, or take a launch override.

Errors

Every error is JSON with an error string. Some carry more fields:

{ "error": "This credential does not carry campaigns:send" }
StatusMeansWhat to do
400The body is invalid, or the action is not allowed in this state (for example editing a campaign that is not a draft).Fix the request. Do not retry unchanged.
401No token, or an unknown, malformed or revoked one. Body: {"error":"Authentication required"}.Stop. Ask a person for a valid credential.
403The credential lacks the scope, or the action is for people only.Stop. Do not look for a way around it.
404Not found in this organization.Check the id.
409The campaign changed while you were acting on it.Read it again, then decide.
422Refused on purpose: missing or stale approval, or content that would harm the recipient. Carries blocked: true and often code.Read code. Most need a person.
429Rate limited. Has a retry-after header in seconds.Wait that long.
500Server error. {"error":"Internal server error"}.Retry later with backoff. Nothing you sent is half-applied in a way that loses mail.

Endpoints

MethodPathScopeDoes
GET/api/campaignscampaigns:readThe 100 newest campaigns, each with stats.
POST/api/campaignscampaigns:writeCreate a draft.
GET/api/campaigns/:idcampaigns:readOne campaign, its stats, approval and a sample of 50 recipients. Add ?approvalDigest=1 for the digest.
PATCH/api/campaigns/:idcampaigns:writeEdit a draft.
DELETE/api/campaigns/:idcampaigns:writeDelete a draft. Sent campaigns cannot be deleted.
GET/api/campaigns/:id/recipientscampaigns:readSearch the list: ?q= and ?take= (default 50).
POST/api/campaigns/:id/recipientscampaigns:writeAdd up to 5,000 recipients per call. Safe to repeat.
PATCH/api/campaigns/:id/recipients/:recipientIdcampaigns:writeRecord how a reply went.
GET/api/campaigns/:id/previewcampaigns:readDry run: what a launch would do, with warnings.
POST/api/campaigns/:id/previewcampaigns:readRender the mail for one recipient or sample fields.
POST/api/campaigns/:id/test-sendpeople onlySends a test to the signed-in person. A credential gets 403.
POST/api/campaigns/:id/sendcampaigns:sendLaunch, with the approval attached. Answers 202.
GET/api/campaigns/send-limitscampaigns:readToday’s sending ceilings and how much is left.

List campaigns

curl -s https://nexmail.nexprove.com/api/campaigns \
  -H "Authorization: Bearer $NEXMAIL_TOKEN"

200

{ "campaigns": [ { "id": "CAMPAIGN_ID", "name": "October update", "status": "draft",
  "contentMode": "template", "fromAddress": "news@example.com", "stats": { "total": 0, "pending": 0 } } ] }

status is one of draft, sending, sent, failed.

Create a draft

Fields: name (required), subject (required), bodyHtml and/or bodyText (at least one), from (required: an address on a verified domain of this organization), fromName, subjectB (a second subject to A/B test). Merge fields are written {{firstName}} and filled from each recipient’s fields.

curl -s -X POST https://nexmail.nexprove.com/api/campaigns \
  -H "Authorization: Bearer $NEXMAIL_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"October update","subject":"What changed in October",
       "bodyHtml":"<p>Hi {{firstName}},</p><p>Here is what changed.</p>",
       "bodyText":"Hi {{firstName}},\n\nHere is what changed.",
       "from":"news@example.com","fromName":"Example Co"}'

201

{ "campaign": { "id": "CAMPAIGN_ID", "name": "October update", "status": "draft" } }

Edit a draft

Send only the fields to change: name, subject, bodyHtml, bodyText, from, fromName, subjectB (null turns the test off). Anything but a draft answers 400.

PATCH /api/campaigns/:id

{ "subject": "What changed this month" }

Add recipients

Up to 5,000 per call; send a long list in several calls. Repeating a call adds nobody twice. Addresses on the organization’s suppression list (unsubscribed, bounced, complained) come back in suppressed and are never mailed.

POST /api/campaigns/:id/recipients

{ "recipients": [
  { "email": "ada@example.org", "fields": { "firstName": "Ada" } },
  { "email": "lin@example.org", "fields": { "firstName": "Lin" } }
] }

200

{ "added": 2, "skipped": 0, "suppressed": [] }

Adding people after a campaign was approved needs a fresh approval for the addition: send the same authorisation block as a launch, without launchedBy, carrying the digest read just before the person approved. Without it the answer is 422 addition_requires_approval.

Dry run and preview

GET /api/campaigns/:id/preview reports what a launch would do: counts, recipients missing merge fields, content that would be refused, which transports would carry it, and plain-language warnings. Show every warning to the person approving.

200 (shortened)

{ "campaignId": "CAMPAIGN_ID", "status": "draft", "subjectTest": false,
  "stats": { "total": 2, "pending": 2 },
  "warnings": [ "Launching this campaign over a machine credential requires an authorisation block naming the person who approved it. …" ] }

POST /api/campaigns/:id/preview renders the actual mail. Body (all optional): recipientId, or fields for sample data, and variant ("A" or "B"). The answer has subject, html, text, missing (merge fields it could not fill) and refusals.

POST /api/campaigns/:id/preview

{ "fields": { "firstName": "Ada" } }

Test send

A test goes to the signed-in person’s own inbox, so it needs a person in the Nexmail app: a credential gets 403. Each person can send 10 tests an hour; the 11th answers 429 with retry-after. Ask the approver to send themselves a test from the campaign page.

Read the approval digest

Right before a person approves, read the campaign with ?approvalDigest=1. The digest fingerprints the content (subject, bodies, sender, click tracking, test subject) and the full recipient list. Keep the object exactly as returned.

curl -s "https://nexmail.nexprove.com/api/campaigns/CAMPAIGN_ID?approvalDigest=1" \
  -H "Authorization: Bearer $NEXMAIL_TOKEN"

200 (shortened)

{ "campaign": { "id": "CAMPAIGN_ID", "status": "draft" },
  "approvalDigest": { "content": "7285e7f5…64 hex", "recipients": "6b5e4a64…64 hex" } }

Launch

The authorisation block is required from a credential. approvedBy is the person who approved; launchedBy is who pressed launch (send the same person twice if one person did both). approvedAt is when they approved: not in the future, not older than 30 days. Unknown keys are rejected.

POST /api/campaigns/:id/send

{
  "authorisation": {
    "approvedBy": { "ref": "crm-user-9f3", "name": "Dana Okafor" },
    "approvedAt": "2026-10-14T09:58:02.000Z",
    "launchedBy": { "ref": "crm-user-9f3", "name": "Dana Okafor" },
    "digest": { "content": "7285e7f5…", "recipients": "6b5e4a64…" }
  },
  "allowMissingFields": false
}

202

{ "queued": true, "campaignId": "CAMPAIGN_ID", "pending": 2,
  "approvedBy": { "source": "crm_assertion", "ref": "crm-user-9f3", "name": "Dana Okafor", "asserted": true },
  "launchedBy": { "source": "crm_assertion", "ref": "crm-user-9f3", "name": "Dana Okafor", "asserted": true },
  "approverLaunchedIt": true }

allowMissingFields: true sends even where a merge field is empty. Leave it false unless a person chose otherwise. overrideUnauthenticatedTransport and overrideUnroutedReplyTo exist for signed-in administrators only; a credential that sets either gets 403.

Launch refusals that need a person (422, blocked: true):

codeMeansDo
missing_authorisationNo authorisation block.Your request is incomplete. Fix it; no person needed.
stale_approvalThe campaign or its list changed after the approval. mismatched says which (content, recipients).Show the person the current campaign (the 422 carries the new approvalDigest), get a new approval, launch again.
approval_expiredapprovedAt is more than 30 days old.Ask the person again.
approval_in_futureapprovedAt is more than 5 minutes ahead of Nexmail’s clock.Fix the clock or the field.

A 422 without code but with refusals means the content itself is blocked (for example a link whose text names a different site than it opens). Edit the draft; it cannot be overridden.

After launch

Poll GET /api/campaigns/:id (once a minute is plenty). Read each recipient’s disposition.state, not status: it is one of pending, queued, held (waiting for a later window, with until), sending, retrying, dispatched (handed to a provider), failed, suppressed or unknown. Only failed is a failure. held alongside stats says how many are waiting and when the first goes. If every provider refuses at launch, the campaign stays draft with everyone pending; launching again later is safe and adds nobody twice.

Record a reply outcome

PATCH /api/campaigns/:id/recipients/:recipientId

{ "disposition": "meeting_booked" }

disposition is interested, meeting_booked, not_interested, or null to clear it.

Send limits

curl -s "https://nexmail.nexprove.com/api/campaigns/send-limits?from=news@example.com" \
  -H "Authorization: Bearer $NEXMAIL_TOKEN"

200 (shortened)

{ "window": { "day": "2026-10-14", "timezone": "UTC", "nextWindowStart": "2026-10-15T00:00:00.000Z" },
  "axes": [ { "scope": "global", "label": "the gateway-wide daily limit", "limit": 500, "used": 120, "remaining": 380, "exhausted": false } ],
  "effective": { "scope": "global", "remaining": 380 },
  "unresolved": ["transport"], "exhausted": false, "warmup": null, "deferredCount": 0 }

effective: null with an empty unresolved means no limit is set. provider is optional (resend, smtp, brevo, smtp2go, outreach, ses); an unknown one answers 400.

The safety model

  • Approval digest. A launch carries the digest a person saw. Nexmail recomputes it from its own data and refuses if anything moved. It proves what was approved; the approver’s name is your word, recorded as crm_assertion.
  • Never lose mail. Every message is saved before it is queued. When no provider is available, mail waits in the queue; it is not marked failed.
  • Suppression. Unsubscribed, bounced and complained addresses are never mailed, even if you add them. If the list cannot be read, bulk mail is held rather than sent blind.
  • Daily ceilings. Per organization, per provider and per domain, plus a warm-up ramp for new domains. Mail over a ceiling waits for the next window; it does not fail and does not use up retries.
  • Emergency stop. An administrator can stop outgoing mail in the app. It acts as a ceiling of zero: send-limits shows an axis with scope: "stop", limit: 0, exhausted: true, and mail waits until it is lifted. A credential cannot lift it.
  • Content checks. Executable attachments, links that lie about where they go and spoofed sender names are refused before sending.
  • Rate limits. Test sends: 10 per person per hour (429 with retry-after).
  • Audit log. Every create, edit, addition, launch and refusal is recorded against the credential.

Recommended workflow

  1. Check limits. GET /api/campaigns/send-limits?from=…. If exhausted is true, tell the person mail will wait.
  2. Create the draft. POST /api/campaigns. Save the id.
  3. Add recipients. POST /api/campaigns/:id/recipients in pages of up to 5,000. Report suppressed to the person.
  4. Preview. GET /api/campaigns/:id/preview and POST /api/campaigns/:id/preview for one or two recipients. Fix every missing field and refusal by editing the draft.
  5. Test send. Ask the person to send themselves a test from the campaign page in Nexmail and read it in their inbox.
  6. Human approval. Read GET /api/campaigns/:id?approvalDigest=1. Show the person the subject, body preview, recipient count and every warning. Record who approved, and when.
  7. Send. POST /api/campaigns/:id/send with the authorisation block and the digest from step 6. Expect 202.
  8. Watch. Poll GET /api/campaigns/:id and report progress. On a 422, follow the table above; do not loop.

Not yet available

These are not open to credentials today. Do not call them or invent paths for them:

  • Reading the inbox, conversations or messages.
  • Replying to or composing individual emails.
  • Contacts and contact lists.
  • Settings, domains, providers, routes and teammates.
  • An MCP server. It is planned, not built.

For language models