Send an email
POSThttps://api.9bits.net/email/ng/v1/messages
Requires the email:send scope.
Request
Section titled “Request”| Field | Type | Required | Description |
|---|---|---|---|
from | object | ✅ | Sender — { "email": "…", "name": "…" }. The domain must be verified. |
to | array | ✅ | Recipients, up to 50 per message. Each is { "email": "…", "name": "…" }. |
reply_to | string | — | Reply-To address. |
subject | string | — | Subject line. |
html | string | — | HTML body. |
text | string | — | Plain-text body. |
template_id | integer | — | Send a stored template by ID. |
template_name | string | — | Send a stored template by name. |
variables | object | — | Values to interpolate into the template. |
track_opens | boolean | — | Enable open tracking. |
track_clicks | boolean | — | Enable click tracking. |
tags | array | — | Labels for filtering in stats. |
Examples
Section titled “Examples”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"] }'import { randomUUID } from 'node:crypto';
const res = await fetch('https://api.9bits.net/email/ng/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NINEBITS_EMAIL_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': randomUUID(), }, body: JSON.stringify({ 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'], }),});
const msg = await res.json();if (!res.ok) throw new Error(msg.error.code);
console.log(msg.id, msg.status); // → "…", "queued"import os, uuid, requests
res = requests.post( "https://api.9bits.net/email/ng/v1/messages", headers={ "Authorization": f"Bearer {os.environ['NINEBITS_EMAIL_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "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"], }, timeout=10,)res.raise_for_status()print(res.json()["id"])Using a template
Section titled “Using a template”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.
Response
Section titled “Response”{ "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).
Idempotency
Section titled “Idempotency”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.
Errors
Section titled “Errors”| Status | Typical code | Meaning |
|---|---|---|
402 | insufficient_balance | Not enough credit. |
403 | insufficient_scope | The key lacks email:send. |
403 | domain_not_verified | The from domain isn’t verified. |
409 | — | Idempotency conflict — same key, different payload. |
422 | validation_error | Bad payload. Check the fields object for specifics. |
429 | rate_limit_exceeded | Back off; honour Retry-After. |