Rules for agents
Follow these exactly. They are how the API is built, not suggestions.
- Use only the endpoints on this page. Do not guess other paths.
- 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.
- Branch on the HTTP status and on
codewhen present. Never on the wording oferror; it changes. - A
202from a launch means queued, not sent. Read the campaign afterwards to see what happened. - Never retry a
422with a freshly read digest unless a person approved again. That defeats the approval. - Treat a held or deferred recipient as waiting, not failed. Nexmail never drops mail; it waits.
- Never print, log or store the token anywhere but a secret store.
Get a credential
- An administrator signs in to Nexmail and opens Settings → Credentials.
- They create a credential, name it after the agent (for example
crm-agent), and tick only the scopes it needs. - 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/jsonBase URL: https://nexmail.nexprove.com. Every path below is relative to it.
Scopes
| Scope | Allows |
|---|---|
campaigns:read | List 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:write | Create, edit and delete draft campaigns; add recipients; mark reply outcomes. |
campaigns:send | Launch a campaign, with a person’s approval attached. |
campaigns:prerendered | Create 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" }| Status | Means | What to do |
|---|---|---|
| 400 | The 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. |
| 401 | No token, or an unknown, malformed or revoked one. Body: {"error":"Authentication required"}. | Stop. Ask a person for a valid credential. |
| 403 | The credential lacks the scope, or the action is for people only. | Stop. Do not look for a way around it. |
| 404 | Not found in this organization. | Check the id. |
| 409 | The campaign changed while you were acting on it. | Read it again, then decide. |
| 422 | Refused 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. |
| 429 | Rate limited. Has a retry-after header in seconds. | Wait that long. |
| 500 | Server error. {"error":"Internal server error"}. | Retry later with backoff. Nothing you sent is half-applied in a way that loses mail. |
Endpoints
| Method | Path | Scope | Does |
|---|---|---|---|
| GET | /api/campaigns | campaigns:read | The 100 newest campaigns, each with stats. |
| POST | /api/campaigns | campaigns:write | Create a draft. |
| GET | /api/campaigns/:id | campaigns:read | One campaign, its stats, approval and a sample of 50 recipients. Add ?approvalDigest=1 for the digest. |
| PATCH | /api/campaigns/:id | campaigns:write | Edit a draft. |
| DELETE | /api/campaigns/:id | campaigns:write | Delete a draft. Sent campaigns cannot be deleted. |
| GET | /api/campaigns/:id/recipients | campaigns:read | Search the list: ?q= and ?take= (default 50). |
| POST | /api/campaigns/:id/recipients | campaigns:write | Add up to 5,000 recipients per call. Safe to repeat. |
| PATCH | /api/campaigns/:id/recipients/:recipientId | campaigns:write | Record how a reply went. |
| GET | /api/campaigns/:id/preview | campaigns:read | Dry run: what a launch would do, with warnings. |
| POST | /api/campaigns/:id/preview | campaigns:read | Render the mail for one recipient or sample fields. |
| POST | /api/campaigns/:id/test-send | people only | Sends a test to the signed-in person. A credential gets 403. |
| POST | /api/campaigns/:id/send | campaigns:send | Launch, with the approval attached. Answers 202. |
| GET | /api/campaigns/send-limits | campaigns:read | Today’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):
code | Means | Do |
|---|---|---|
missing_authorisation | No authorisation block. | Your request is incomplete. Fix it; no person needed. |
stale_approval | The 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_expired | approvedAt is more than 30 days old. | Ask the person again. |
approval_in_future | approvedAt 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-limitsshows an axis withscope: "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
- Check limits.
GET /api/campaigns/send-limits?from=…. Ifexhaustedis true, tell the person mail will wait. - Create the draft.
POST /api/campaigns. Save theid. - Add recipients.
POST /api/campaigns/:id/recipientsin pages of up to 5,000. Reportsuppressedto the person. - Preview.
GET /api/campaigns/:id/previewandPOST /api/campaigns/:id/previewfor one or two recipients. Fix every missing field and refusal by editing the draft. - Test send. Ask the person to send themselves a test from the campaign page in Nexmail and read it in their inbox.
- 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. - Send.
POST /api/campaigns/:id/sendwith theauthorisationblock and the digest from step 6. Expect 202. - Watch. Poll
GET /api/campaigns/:idand 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
- This page as Markdown: /docs/ai-agents.md.
- A summary of Nexmail and links to every page: /llms.txt.
- Everything in one file, including this page: /llms-full.txt.