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.

StatustypeMeaning
400invalid_requestA field is missing or malformed. The message names it.
401authentication_errorNo key, or an unknown key.
402insufficient_balanceThe send costs more than your balance. Top up first.
403sender_not_verifiedThe e-mail from domain is not verified on your account.
429rate_limitedMore than 600 requests per minute on the account.
400suppressedEvery recipient is on your suppression list.
502The 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

POST/v1/messages
FieldRequiredNotes
channelyes"email"
toyesOne address or an array of up to 50. Billed per recipient.
subjectyesUp to 998 characters.
text / htmlone ofPlain text, HTML, or both. Both are sent as alternatives.
fromno"Name <[email protected]>". The domain must be verified. Default: Your name <[email protected]>.
reply_tonoAddress or array of up to 5.
cc / bccnoArrays of addresses. to + cc + bcc up to 50; billed per recipient.
attachmentsnoUp 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.
headersnoExtra headers, e.g. {"X-Entity-Ref-ID": "inv_42"}. Standard headers cannot be overridden.
metadatanoObject 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

POST/v1/messages
FieldRequiredNotes
channelyes"sms"
toyesOne number or an array of up to 50, in E.164 (+32470123456). Invalid numbers are rejected before anything is sent.
textyesUp to 918 characters. 160 GSM characters per segment (70 with emoji or non-Latin script); longer texts are split and billed per segment.
default_countrynoISO code used to parse national numbers, e.g. "FR" for 0612345678.
fromnoAlphanumeric sender, 3 to 11 characters. Must be approved on your account first (write to [email protected]). Default: a shared short number.
metadatanoObject 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": "…" }
Transactional only. Marketing SMS, and anything that needs an unsubscribe clause, is not accepted on this API.

Read a message

GET/v1/messages/{id}

Returns the message with its current status: scheduled, sent, delivered, bounced, complained, failed or cancelled, plus error when there is one.

List messages

GET/v1/messages?limit=50&cursor=…

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.

POST/v1/templates
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.

GET/v1/stats?from=2026-09-01&to=2026-09-30&group_by=tag
{ "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

POST/v1/messages/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:

DELETE/v1/messages/{id}
{ "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:

GET/v1/suppressions
POST/v1/suppressions

Body { "email": "[email protected]", "detail": "asked to stop" }.

DELETE/v1/suppressions/{email}

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.

POST/v1/domains
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.

GET/v1/domains/{domain}

Re-checks and returns status: "verified" once the records resolve.

GET/v1/domains
DELETE/v1/domains/{domain}

Status events

PUT/v1/account/webhook
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

GET/v1/account
{ "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

GET/v1/account/keys
POST/v1/account/keys

Body { "label": "staging" }. The full key is returned once.

DELETE/v1/account/keys/{id}

The last remaining key cannot be revoked; create a replacement first.

Top-ups

POST/v1/topups
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 €.

GET/v1/topups

Prices

GET/v1/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

PlanPer monthE-mail per 1,000SMS
payg0 €2.00 €table price
starter5 €1.00 €−5 %
pro12 €0.60 €−10 %
scale29 €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.