Get started
Responses and errors
Every answer has one of two shapes. Test `error.code`, never the text.
Success: data
HTTP/1.1 200 OKX-RateLimit-Limit: 600X-RateLimit-Remaining: 598X-RateLimit-Reset: 1790000060{ "data": { "items": [ { "id": "9c1f…", "status": "open" } ], "nextCursor": "MjAyNi0wOS0xOFQw…", "hasMore": true }}Refusal: error
HTTP/1.1 422 Unprocessable Entity{ "error": { "status": 422, "code": "recipient_invalid", "message": "`to` must be a number in international format (E.164), `recipient` a WhatsApp user id `wa:…`, and never both.", "detail": "`to` must be a number in international format (E.164), for example +33612345678.", "param": "to", "doc_url": "https://nodium.io/en/docs/api/guide/responses#recipient_invalid", "requestId": "a1b2c3d4" }}codeis stable and documented below: test it, never the text.messageis the same English sentence for a given code.detailis the specific reason for this call, in English. A few refusals not translated yet still carry a Frenchdetail;codeandmessageare always English. SendAccept-Language: frto ask for French where a translation exists. Forrate_limited,quota_exceededandplan_limit,detailisnull:messageanddatacarry everything.paramnames the field of the body or query that was refused (to,type,location.latitude,variables.1), or isnullwhen the refusal is not about one field. It is set on the422refusals of sending, and on most of the other validated fields.doc_urllinks to the line of this page that explainscode; it isnullfor a code that is not listed below.requestIdidentifies the call in our logs and inGET /api-requests: quote it if you need help.- Some refusals carry more facts in
data—template_holes_unfilledlists the unfilled placeholders,tenant_existsgives the existing client id,window_closedthe state of the window.
Lists and pagination
A list answers data.items. A list that pages takes limit and cursor and also returns nextCursor (null on the last page), hasMore, and total on the first page when the list counts. Pass nextCursor back as cursor until it is null; cursors are opaque, do not build them. A list returned whole (channels, API keys, Flows…) carries only items, and its description says how many it holds at most.
let cursordo { const url = new URL('https://nodium.io/api/v1/conversations') url.searchParams.set('limit', '100') if (cursor) url.searchParams.set('cursor', cursor) const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.NODIUM_KEY}` } }) const body = await res.json() if (body.error) throw new Error(`${body.error.code}: ${body.error.message}`) for (const conversation of body.data.items) handle(conversation) cursor = body.data.nextCursor} while (cursor)Three routes keep a shape of their own: GET /events answers data and next_after (a position in the numbered event log, not an opaque cursor), GET /templates answers templates and summary, and GET /webhooks answers events, subscriptions and deliveries.
Field names
REST requests and responses use camelCase (channelId, nextCursor). Webhook payloads and entries of the event log use snake_case (workspace_id, occurred_at, error_code), and so does next_after: they are the same event envelopes, with a contract of their own. Within one family a field is always spelled the same way; the two families are not translated into each other.
Error codes
Every code below is an anchor of this page: the doc_url of a refusal points to its line.
bad_request- The request could not be read. Check that the body is valid JSON.
forbidden- This call is not allowed.
not_found- Not found — or it belongs to another workspace.
method_not_allowed- This method is not allowed on this address.
conflict- The request conflicts with the current state of the resource.
payload_too_large- The request body is too large.
unsupported_media_type- This content type is not accepted here.
invalid_request- A field is missing or invalid.
detailsays which one. rate_limited- Too many calls: 600 per minute per client (per key when it is pinned to a client), 6,000 per key. Wait and retry.
internal_error- Something went wrong on our side. Retry; if it persists, send us the
requestId. upstream_error- The messaging provider refused or failed.
detailcarries its reason. upstream_timeout- The messaging provider did not answer in time. Retry.
api_key_revoked- This API key has been revoked. Create a new one in the console.
api_key_expired- This API key has expired. Create a new one in the console.
api_keys_not_installed- API keys are not available on this instance.
window_closed- The 24-hour customer service window is closed: only an approved template can be sent.
idempotency_key_requiredidempotencyKeyis required: 8 to 200 characters, unique per message.idempotency_key_conflict- This
idempotencyKeywas already used for a different request: a retry must carry exactly the same content, otherwise use a new key. no_recipient- The contact has no address on this channel: no phone number and no WhatsApp user id (
wa:…). recipient_required- Name the person to write to:
to(a phone number) orrecipient(a WhatsApp user idwa:…). recipient_invalidtomust be a number in international format (E.164),recipienta WhatsApp user idwa:…, and never both.invalid_typetypeis missing or unknown: text, interactive, location, contacts, template, image, video, audio, document or reaction.invalid_field- A field was given that does not belong to this
type. channel_inactive- This number is switched off: switch it on before sending from it.
media_requiredmediaIdis required: upload the file first withPOST /media.media_not_found- No such file for this client, or its
mediaIdhas expired (7 days). Upload it again. media_type_mismatch- The file behind
mediaIdis not of the kind named bytype. text_too_long- The text is over 4,096 characters, the WhatsApp limit. Nothing is stored.
caption_too_long- The caption is over 1,024 characters, the WhatsApp limit.
template_required- A template is required here.
template_not_approved- This template is not approved by Meta yet.
template_header_invalid- The media header does not fit: an unknown sample, a text and a media header together, or a header file for a template without a media header.
template_variables_invalid- The template variables do not match its placeholders.
template_holes_unfilled- Some template placeholders have no value.
template_not_applicable- This template cannot be sent on this conversation.
template_locked- This template is under review (or Meta disabled it) and cannot be changed now.
template_category_locked- Approved or paused at a client, this template keeps its category: Meta does not let it change.
template_edit_refused- Meta refused the correction everywhere (limit of one per 24 hours or ten per 30 days, text refused…):
data.correctionssays why. Nothing changed. plan_limit- Your plan's limit is reached:
data.keynames it (clients, numbers, apps, webhooks or members),data.limitis the number included. A limit of 0 means the plan does not include it at all (the Build plan includes no clients besides "My company"). Upgrade your plan to add more. quota_exceeded- Your plan's monthly allowance of messages is used up (on a paid plan: your overage cap is reached). Sends resume at
data.resetsAt; receiving goes on. Upgrade your plan, or raise your overage cap. queue_not_supported- This message cannot be queued. Send it without
queue. webhook_url_not_https- The webhook address must start with https://.
file_missing- No file was sent.
file_too_large- The file is too large for this channel.
file_type_refused- This file type is refused.
caption_unsupported- This kind of file cannot carry a caption.
channel_requiredchannelIdis required.unknown_channel- No such channel in this workspace.
no_channel- No active channel can reach this contact.
meta_token_missing- The WhatsApp connection is not configured on this instance.
identity_taken- Another contact already has this phone number or email address.
confirmation_mismatch- The confirmation does not match the contact.
consent_missing- The contact has not agreed to receive marketing messages, or has opted out.
contact_blocked- This contact is blocked.
not_whatsapp- This only works on WhatsApp conversations.
store_failed- The event could not be stored. It will be retried.
cursor_expired- This cursor is older than the oldest event kept (30 days): some events were purged before they were read. Restart from
after=0, or from the head (call withoutafter). tenant_required- This account has no default client: name the tenant with the
Nodium-Tenantheader (tenant id or your externalId). tenant_invalid- The
Nodium-Tenantheader could not be read. tenant_not_found- No such tenant in your account — or out of this key's reach.
tenant_closed- This tenant is suspended or closed.
tenant_exists- A tenant with this externalId already exists;
tenantIdis indetail. all_clients_required- This call covers every client of your account: it needs a key that is not pinned to a client.
account_suspended- This account is suspended.
link_not_found- This connect link does not exist.
link_used- This connect link has already been used.
link_expired- This connect link has expired. Create a new one.
link_closed- This connect link can no longer be used: its tenant was closed.
own_key- This key would revoke itself. Add
?confirmSelf=trueto do it on purpose. already_revoked- This key is already revoked.
broadcast_finished- This broadcast has already finished; there is nothing left to cancel.
broadcast_not_paused- This broadcast is not paused: only a paused broadcast can be resumed.
broadcast_exceeds_messaging_limit- This broadcast reaches more recipients than the number's messaging tier allows per 24 hours:
data.limit,data.recipientsanddata.tiersay how many. Split it into smaller broadcasts, or wait for a higher tier. openapi_invalid- The document is not a readable OpenAPI 3 document (JSON):
detailsays why. no_template- No such template for this client.
template_exists- A template with this name and language already exists in your account;
templateIdis indetail. no_waba- None of your clients has a WhatsApp number connected yet.
invalid_cursor- The
cursoris not one this API issued. Pass back anextCursorexactly as received, or omitcursorto start over. text_required- The message text is empty:
textis required fortype: "text". invalid_formformis missing or invalid fortype: "interactive":detailsays which part,paramnames the field.invalid_locationlocationis missing or invalid: latitude and longitude are required, within GPS bounds.invalid_contactscontactsis missing or invalid: one card at most, each withname.formatted_name.emoji_requiredemojiis required fortype: "reaction"(an empty string removes the reaction).message_not_found- No such message in this client's conversations (or its
messageIdis not an id). scheduled_at_invalidscheduledAtis not a valid ISO 8601 date, or is less than a minute ahead.no_identity- Neither a phone number nor an email address was given.
broadcast_started- This broadcast has started: its recipients and its start time can no longer change.
broadcast_empty- This broadcast has no recipient yet. Add some with
POST /broadcasts/{id}/recipients. username_transfer_required- Another number of the same Meta business portfolio has this username. Send again with
transfer: trueto move it here.