API reference
Base URL https://api.notifpool.com. JSON in, JSON out. All prices in EUR, VAT excluded. Also available as OpenAPI 3.1.
Authentication
Every request under /v1 carries your key as a bearer token. Keys start with np_live_. Get one from the form on the home page; create more with POST /v1/account/keys.
Authorization: Bearer np_live_...
Errors
Errors return an HTTP status and one object: { "error": { "type", "message" } }. The message says what to change.
| Status | type | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed. The message names it. |
| 401 | authentication_error | No key, or an unknown key. |
| 402 | insufficient_balance | The send costs more than your balance. Top up first. |
| 403 | sender_not_verified | The e-mail from domain is not verified on your account. |
| 429 | rate_limited | More than 600 requests per minute on the account. |
| 400 | suppressed | Every recipient is on your suppression list. |
| 502 | — | The carrier or mail provider refused the message. Nothing is charged; the message has status: "failed" and an error. |
Idempotency
Add an Idempotency-Key header (any string up to 200 characters) to POST /v1/messages. For 24 hours, a repeated key returns the original message with status 200 instead of sending again. Use your own order id, OTP id, or a UUID.
Send an e-mail
| Field | Required | Notes |
|---|---|---|
| channel | yes | "email" |
| to | yes | One address or an array of up to 50. Billed per recipient. |
| subject | yes | Up to 998 characters. |
| text / html | one of | Plain text, HTML, or both. Both are sent as alternatives. |
| from | no | "Name <[email protected]>". The domain must be verified. Default: Your name <[email protected]>. |
| reply_to | no | Address or array of up to 5. |
| cc / bcc | no | Arrays of addresses. to + cc + bcc up to 50; billed per recipient. |
| attachments | no | Up to 10 files, 10 MB in total. Each: filename plus either content (base64) or url (https, downloaded by us), optional content_type. Executables are refused. |
| headers | no | Extra headers, e.g. {"X-Entity-Ref-ID": "inv_42"}. Standard headers cannot be overridden. |
| metadata | no | Object of strings, returned on every read and webhook. |
curl https://api.notifpool.com/v1/messages \
-H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"to": ["[email protected]"],
"cc": ["[email protected]"],
"from": "Acme <[email protected]>",
"reply_to": "[email protected]",
"subject": "Invoice 2026-091",
"html": "<p>Your invoice is attached.</p>",
"text": "Your invoice is attached.",
"attachments": [
{ "filename": "invoice-2026-091.pdf", "url": "https://acme.io/inv/2026-091.pdf" }
],
"metadata": { "invoice": "2026-091" }
}'
For a file you already hold in memory, send it inline: { "filename": "quote.pdf", "content": "<base64>" }. Inline images for HTML: add "inline": true, "cid": "logo" and reference <img src="cid:logo">.
Response 202 Accepted:
{
"id": "msg_4b1e9f0c2a7d5e83b6f1",
"channel": "email",
"to": ["[email protected]"],
"from": "Acme <[email protected]>",
"subject": "Invoice 2026-091",
"status": "sent",
"cost": 0.002,
"created_at": "2026-09-06T20:14:02.113Z",
"metadata": { "invoice": "2026-091" }
}
Send an SMS
| Field | Required | Notes |
|---|---|---|
| channel | yes | "sms" |
| to | yes | One number or an array of up to 50, in E.164 (+32470123456). Invalid numbers are rejected before anything is sent. |
| text | yes | Up to 918 characters. 160 GSM characters per segment (70 with emoji or non-Latin script); longer texts are split and billed per segment. |
| default_country | no | ISO code used to parse national numbers, e.g. "FR" for 0612345678. |
| from | no | Alphanumeric sender, 3 to 11 characters. Must be approved on your account first (write to [email protected]). Default: a shared short number. |
| metadata | no | Object of strings. |
curl https://api.notifpool.com/v1/messages \
-H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-7f3a91" \
-d '{ "channel": "sms", "to": "+32470123456", "text": "Your code is 482 913. Valid 5 minutes." }'
{ "id": "msg_b73a…", "channel": "sms", "to": ["+32470123456"], "status": "sent",
"cost": 0.075, "segments": 1, "country": "BE", "created_at": "…" }
Read a message
Returns the message with its current status: scheduled, sent, delivered, bounced, complained, failed or cancelled, plus error when there is one.
List messages
Newest first. next_cursor is null on the last page.
Templates
Store a message once, send it with variables. Variables are {{name}}, nested {{order.total}}, with an optional default {{name | Customer}}. Values are HTML-escaped inside html, raw elsewhere.
curl https://api.notifpool.com/v1/templates -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{
"name": "invoice",
"subject": "Invoice {{number}} from {{company | Acme}}",
"html": "<p>Hello {{customer.name}},</p><p>Invoice {{number}} for {{total}} € is attached.</p>",
"text": "Hello {{customer.name}}, invoice {{number}} for {{total}} € is attached.",
"sms_text": "{{company | Acme}}: invoice {{number}} ({{total}} €) sent to your inbox.",
"from": "Acme <[email protected]>",
"tags": ["invoice"]
}'
A template can hold an e-mail (subject + text/html), an SMS (sms_text), or both. Then send it:
curl https://api.notifpool.com/v1/messages -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{
"template": "invoice",
"channel": "email",
"to": "[email protected]",
"variables": { "number": "2026-091", "total": "149.00", "customer": { "name": "Ada" } },
"attachments": [{ "filename": "invoice-2026-091.pdf", "url": "https://acme.io/inv/2026-091.pdf" }]
}'
channel defaults to e-mail when the template has a subject, else SMS. Any field you pass in the body (subject, text, from, tags…) overrides the template. Other endpoints: GET /v1/templates, GET /v1/templates/{name}, PUT /v1/templates/{name} (update), DELETE /v1/templates/{name}, and POST /v1/templates/{name}/render with { "variables": {…} } to preview without sending.
Tags and stats
Add "tags": ["invoice", "eu"] (up to 5, lowercase) to any message, or set them on a template. Filter the log with GET /v1/messages?tag=invoice, also &status=bounced and &channel=sms.
{ "from": "2026-09-01", "to": "2026-09-30", "group_by": "tag",
"total": { "sent": 5120, "delivered": 5071, "bounced": 31, "complained": 2, "failed": 16, "cost": 3.2984 },
"data": [
{ "tag": "invoice", "sent": 1240, "delivered": 1236, "bounced": 3, "complained": 0, "failed": 1, "cost": 0.744 },
{ "tag": "otp", "sent": 880, "delivered": 871, "bounced": 0, "complained": 0, "failed": 9, "cost": 66.0 }
] }
group_by is day (default), channel or tag. Counts are per recipient; the default range is the last 30 days.
Batch
Up to 100 messages in one call, e-mail and SMS mixed. Each entry is a normal message body and may carry its own idempotency_key. Entries are processed five at a time; each gets its own result, so one bad address never blocks the others.
curl https://api.notifpool.com/v1/messages/batch -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{ "messages": [
{ "template": "invoice", "to": "[email protected]", "variables": { "number": "2026-091" }, "idempotency_key": "inv-2026-091" },
{ "channel": "sms", "to": "+32470123456", "text": "Your code is 482 913." }
] }'
{ "total": 2, "accepted": 2, "rejected": 0,
"data": [ { "index": 0, "ok": true, "http_status": 202, "id": "msg_…", "status": "sent", … }, { "index": 1, "ok": true, "http_status": 202, … } ] }
The call returns 202 when at least one entry was accepted, 400 when none was. Rejected entries carry { "index", "ok": false, "http_status", "error" }.
Scheduling
Add "send_at": "2026-09-08T09:00:00+02:00" (ISO 8601, at least one minute ahead, at most 30 days) to any message. The message is stored with status: "scheduled", nothing is charged yet, and it is sent within a minute of the due time. Cancel it while it is still scheduled:
{ "id": "msg_…", "status": "cancelled" }
Suppression list
An address that bounces permanently or complains is added to your suppression list automatically and skipped on later sends (the message response lists it under suppressed; if every recipient is suppressed the call returns 400 suppressed). Manage the list:
Body { "email": "[email protected]", "detail": "asked to stop" }.
Removing an address lets you send to it again. Do this only when you know the mailbox works.
Sender domains
To send e-mail from your own addresses, add the domain once and publish the DNS records returned. Verification usually completes within minutes.
curl https://api.notifpool.com/v1/domains -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{ "domain": "acme.io" }'
{
"domain": "acme.io",
"status": "pending",
"mail_from": { "domain": "np.acme.io", "status": "pending" },
"records": [
{ "name": "abc123._domainkey.acme.io", "type": "CNAME", "value": "abc123.dkim.amazonses.com", "purpose": "dkim (required)" },
{ "name": "def456._domainkey.acme.io", "type": "CNAME", "value": "def456.dkim.amazonses.com", "purpose": "dkim (required)" },
{ "name": "ghi789._domainkey.acme.io", "type": "CNAME", "value": "ghi789.dkim.amazonses.com", "purpose": "dkim (required)" },
{ "name": "_dmarc.acme.io", "type": "TXT", "value": "v=DMARC1; p=none;", "purpose": "dmarc (recommended)" },
{ "name": "np.acme.io", "type": "MX", "value": "feedback-smtp.eu-west-1.amazonses.com", "priority": 10, "purpose": "return-path (optional)" },
{ "name": "np.acme.io", "type": "TXT", "value": "v=spf1 include:amazonses.com ~all", "purpose": "return-path spf (optional)" }
]
}
The three DKIM records are enough to send. The two np. records are optional: with them, the Return-Path of your messages is on your own domain (better alignment for DMARC), and mail_from.status turns to success.
Re-checks and returns status: "verified" once the records resolve.
Status events
curl -X PUT https://api.notifpool.com/v1/account/webhook -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{ "url": "https://acme.io/hooks/notifpool" }'
# → { "webhook_url": "…", "webhook_secret": "whsec_…" }
Each event is a POST with a JSON body and two headers: X-NotifPool-Event and X-NotifPool-Signature, the hex HMAC-SHA256 of the raw body using your secret. Reply 2xx within 5 seconds; one retry follows otherwise.
{ "event": "message.delivered", "created_at": "…", "data": { "id": "msg_…", "channel": "sms", "status": "delivered", … } }
Events: message.delivered, message.bounced, message.complained, message.failed. Send { "url": null } to stop.
Balance
{ "id": "acc_…", "name": "Acme", "email": "[email protected]", "plan": "pro", "plan_name": "Pro", "balance": 42.1735, "spent": 7.8265,
"sent_email": 5120, "sent_sms": 31, "webhook_url": "https://acme.io/hooks/notifpool", "created_at": "…" }
API keys
Body { "label": "staging" }. The full key is returned once.
The last remaining key cannot be revoked; create a replacement first.
Top-ups
curl https://api.notifpool.com/v1/topups -H "Authorization: Bearer np_live_..." \
-H "Content-Type: application/json" -d '{ "amount": 50 }'
# → { "id": "top_…", "amount": 50, "vat": 10.5, "total": 60.5, "checkout_url": "https://www.mollie.com/checkout/…", "status": "open" }
Open checkout_url (card, Bancontact, iDEAL, SEPA). The balance is credited the moment the payment is confirmed. Packs: 10, 25, 50, 100, 250, 500 €.
Prices
Public, no key needed. Returns the e-mail price and the SMS price per segment for the main countries, for each plan. Every message response also carries its exact cost.
Plans
| Plan | Per month | E-mail per 1,000 | SMS |
|---|---|---|---|
| payg | 0 € | 2.00 € | table price |
| starter | 5 € | 1.00 € | −5 % |
| pro | 12 € | 0.60 € | −10 % |
| scale | 29 € | 0.30 € | −15 % |
Your current plan is in GET /v1/account. To move to a plan, write to [email protected] for now; self-serve subscription is coming.