Skip to content
Get an API key

Get started

Responses and errors

Every answer has one of two shapes. Test `error.code`, never the text.

Success: data

HTTP

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

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" }}
  • code is stable and documented below: test it, never the text. message is the same English sentence for a given code.
  • detail is the specific reason for this call, in English. A few refusals not translated yet still carry a French detail; code and message are always English. Send Accept-Language: fr to ask for French where a translation exists. For rate_limited, quota_exceeded and plan_limit, detail is null: message and data carry everything.
  • param names the field of the body or query that was refused (to, type, location.latitude, variables.1), or is null when the refusal is not about one field. It is set on the 422 refusals of sending, and on most of the other validated fields.
  • doc_url links to the line of this page that explains code; it is null for a code that is not listed below.
  • requestId identifies the call in our logs and in GET /api-requests: quote it if you need help.
  • Some refusals carry more facts in data — template_holes_unfilled lists the unfilled placeholders, tenant_exists gives the existing client id, window_closed the 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.

Node.js

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.
unauthorized
Missing or invalid API key. Send Authorization: Bearer nod_….
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. detail says 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. detail carries its reason.
unavailable
The service is temporarily unavailable. Retry shortly.
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_required
idempotencyKey is required: 8 to 200 characters, unique per message.
idempotency_key_conflict
This idempotencyKey was 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) or recipient (a WhatsApp user id wa:…).
recipient_invalid
to must be a number in international format (E.164), recipient a WhatsApp user id wa:…, and never both.
invalid_type
type is 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_required
mediaId is required: upload the file first with POST /media.
media_not_found
No such file for this client, or its mediaId has expired (7 days). Upload it again.
media_type_mismatch
The file behind mediaId is not of the kind named by type.
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_buttons_invalid
A button value does not match the template's buttons: position, type, or a missing value.
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.corrections says why. Nothing changed.
plan_limit
Your plan's limit is reached: data.key names it (clients, numbers, apps, webhooks or members), data.limit is 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_required
channelId is 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.
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 without after).
tenant_required
This account has no default client: name the tenant with the Nodium-Tenant header (tenant id or your externalId).
tenant_invalid
The Nodium-Tenant header 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; tenantId is in detail.
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.
own_key
This key would revoke itself. Add ?confirmSelf=true to 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.recipients and data.tier say 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): detail says why.
no_template
No such template for this client.
template_exists
A template with this name and language already exists in your account; templateId is in detail.
no_waba
None of your clients has a WhatsApp number connected yet.
invalid_cursor
The cursor is not one this API issued. Pass back a nextCursor exactly as received, or omit cursor to start over.
text_required
The message text is empty: text is required for type: "text".
invalid_form
form is missing or invalid for type: "interactive": detail says which part, param names the field.
invalid_location
location is missing or invalid: latitude and longitude are required, within GPS bounds.
invalid_contacts
contacts is missing or invalid: one card at most, each with name.formatted_name.
emoji_required
emoji is required for type: "reaction" (an empty string removes the reaction).
message_not_found
No such message in this client's conversations (or its messageId is not an id).
quote_unavailable
This message cannot be quoted: it has no provider id.
scheduled_at_invalid
scheduledAt is 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_unavailable
Meta refused this username: it is taken, not allowed, or the account is not eligible yet. detail gives Meta's reason.
username_transfer_required
Another number of the same Meta business portfolio has this username. Send again with transfer: true to move it here.