Skip to content
Get an API key

Get started

Sending

Every send carries your own key against duplicates. For volume, queue.

idempotencyKey

Every send requires an idempotency key, 8 to 200 characters, chosen by you and unique per message — an order number and a step, for example. Give it as the idempotencyKey field of the body, or as the Idempotency-Key request header (the usual place for it); the same rules apply to both. Give it in one place, or in both with the same value: two different values are refused with 422 invalid_request. Sending twice with the same key sends once: the second call returns the first message with duplicate: true, even if the window closed in between. Retry freely after a timeout — with the same request: the same key used for different content (another number, type, text or template values) is refused with 409 idempotency_key_conflict, and nothing is sent. queue and scheduledAt do not count as content. Use a new key for a new message.

What you can send

  • POST /messages — everything goes through this one call, by channelId and the person (to, a phone number in E.164 form, or recipient, their WhatsApp user id wa:…). Nodium finds their conversation, or opens it. type chooses: text; interactive (form: up to 3 buttons, a list of up to 10 rows, a location request, or a Flow that opens a WhatsApp form — see Flows); location (a pin); contacts (one contact card); template (an approved template, inside or outside the window); image, video, audio, document (a file, by mediaId); reaction (messageId, emoji). replyToId quotes an earlier message of the conversation.
  • POST /media — upload a file (multipart, 4 MB at most) and get a mediaId, valid for 7 days, that POST /messages cites. The same mediaId can be sent to many people.
  • POST /conversations/{id}/read, /typing — a read receipt, the typing indicator.
  • POST /broadcasts — one template to many contacts; POST /broadcasts/preview first, POST /broadcasts/{id}/cancel to stop it. The number's messaging tier is honoured, and a broadcast whose sends are refused over and over for an account reason is paused (status: "paused"): fix the cause, then POST /broadcasts/{id}/resume.

queue and scheduledAt

By default a send waits for WhatsApp to accept the message and returns its status. With queue: true it returns at once with HTTP 202 and status: "queued"; the message leaves within a minute. With scheduledAt (ISO 8601, at least one minute ahead) it is stored as scheduled and leaves at that time. Follow it with the message.sent, message.delivered, message.read and message.failed webhooks.

Shell

curl https://nodium.io/api/v1/messages \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "channelId": "<channel id>", "to": "+33612345678", "type": "template", "templateId": "<id>", "variables": { "1": "Karim" }, "idempotencyKey": "campaign-42-contact-981", "queue": true }'# → 202 Accepted, "status": "queued". It leaves within a minute;# message.sent or message.failed tells you how it went.

Retries

When Meta answers with a transient refusal (rate limit or server error), Nodium retries on its own after 30 seconds, 2 minutes and 10 minutes before marking the message failed and emitting message.failed. A permanent refusal fails at once, with the reason in error.

A refused send is still HTTP 200

When WhatsApp refuses a message, the call itself succeeded: POST /messages answers 200 (or 201) with message.status: "failed", not an HTTP error. Test message.status, not only the HTTP code. message.error is the provider's reason, and message.errorInfo explains it: title says what happened, action what to do, and actor who acts — you (your code or console), client (the owner of the number: action is written to be passed on to them as is), nodium, or wait (the refusal is temporary). errorInfo is null when the message did not fail, or failed without a provider code. The message.failed webhook carries the same as error_info.

JSON

{ "data": { "conversationId": "7c1e4a90-0000-4000-8000-000000000002", "message": { "id": "b1a4e7c0-0000-4000-8000-000000000001", "status": "failed", "error": "Meta refused: payment method missing (code 131042)", "errorInfo": { "code": 131042, "title": "Missing or declined payment method on your client's WhatsApp account: Meta blocks paid messages.", "action": "Add a valid payment method to your WhatsApp Business account in WhatsApp Manager, under payment methods. Messages will then go out normally.", "actor": "client" } }, "duplicate": false }}

To check a message later without listing its conversation, GET /messages/{id} returns it as it stands: its current status, errorInfo and ids. Prefer the webhooks to polling it.

Statuses

  • queued, scheduled — waiting to leave.
  • sending — being handed to WhatsApp.
  • sent — WhatsApp accepted it.
  • delivered, read — reported back by WhatsApp. A later status never goes backwards.
  • failed — refused, now or later; error says why and errorInfo explains it.

Template placeholders

A template placeholder is numbered ({{1}}) or named ({{client_name}}). Fill it in variables by the same key: { "1": "Karim" } or { "client_name": "Karim" }. A placeholder left empty is refused with template_holes_unfilled. A template is written for the client the call acts on with POST /templates, then deployed to the WhatsApp accounts of that client's numbers with POST /templates/{id}/deploy (numbers connected later receive it on their own). Meta reviews it account by account: POST /templates/{id}/refresh re-reads the states, and every status or category change arrives as template.status_changed. To send, the name is enough: Nodium looks it up at the client the conversation belongs to and picks the version approved for the number that sends.

Received media

A received file comes with media.url in message.received; download it with GET /messages/{id}/media and your key. A voice note arrives as an audio file: your code listens to it or passes it on.