Skip to content

Send an email

POSThttps://api.9bits.net/email/ng/v1/messages

Requires the email:send scope.

FieldTypeRequiredDescription
fromobjectSender — { "email": "…", "name": "…" }. The domain must be verified.
toarrayRecipients, up to 50 per message. Each is { "email": "…", "name": "…" }.
reply_tostringReply-To address.
subjectstringSubject line.
htmlstringHTML body.
textstringPlain-text body.
template_idintegerSend a stored template by ID.
template_namestringSend a stored template by name.
variablesobjectValues to interpolate into the template.
track_opensbooleanEnable open tracking.
track_clicksbooleanEnable click tracking.
tagsarrayLabels for filtering in stats.
Terminal window
curl -X POST https://api.9bits.net/email/ng/v1/messages \
-H "Authorization: Bearer $NINEBITS_EMAIL_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"from": { "email": "no-reply@acme.com", "name": "Acme" },
"to": [{ "email": "jane@example.com", "name": "Jane" }],
"subject": "Your receipt",
"html": "<p>Thanks for your order, Jane.</p>",
"text": "Thanks for your order, Jane.",
"tags": ["receipt"]
}'

Store the template once, then send by name and pass variables:

{
"from": { "email": "no-reply@acme.com", "name": "Acme" },
"to": [{ "email": "jane@example.com", "name": "Jane" }],
"template_name": "order-shipped",
"variables": {
"first_name": "Jane",
"tracking_url": "https://acme.com/track/AB123"
}
}

Templates keep copy out of your codebase — marketing can change the wording without a deploy.

202 Accepted
{
"object": "message",
"id": "msg_01J8ZC4K2X",
"status": "queued",
"to": ["jane@example.com"],
"amount_charged": 0.42,
"billing_source": "volume",
"currency": "NGN",
"queued_at": "2026-07-14T18:48:14+01:00"
}

202, not 200 — and status is queued. Like SMS, sending is asynchronous: the message is accepted, not yet delivered. Store id to look it up later or to correlate the delivery webhook.

amount_charged and billing_source tell you what this send cost and where it was drawn from (volume, wallet_overage, accrued_overage, wallet_legacy).

POST /messages accepts an Idempotency-Key header. A replayed request returns the original response with Idempotent-Replayed: true on it, and does not send a second email. See Idempotency & retries.

StatusTypical codeMeaning
402insufficient_balanceNot enough credit.
403insufficient_scopeThe key lacks email:send.
403domain_not_verifiedThe from domain isn’t verified.
409Idempotency conflict — same key, different payload.
422validation_errorBad payload. Check the fields object for specifics.
429rate_limit_exceededBack off; honour Retry-After.