Skip to content
Get an API key
Reference

Messages

4 endpoints in this family.

List messages

GET/api/v1/messages

Every message of the client you act on, across conversations, newest first. Narrow it by number, contact, direction or status. To read one thread in order, use GET /conversations/{id}/messages.

Query parameters

  • channelIdstring

    Only the messages of this number.

  • contactIdstring

    Only the messages with this contact.

  • directionstring

    Received or sent.

    inboundoutbound

  • statusstring

    Only messages in this state.

    scheduledqueuedsendingsentdeliveredreadfailed

  • limitinteger

    How many to return, 50 by default, 200 at most.

  • cursorstring

    The nextCursor of the previous page.

Response

A page of messages.

  • data.itemsMessage[]

    The messages, newest first.

  • data.nextCursorstring

    Pass it as cursor for older messages; null at the end.

  • data.hasMoreboolean

    Whether older messages exist.

Refusals

  • 422invalid_requestA filter is not a valid id or value.
curl https://nodium.io/api/v1/messages \ -H "Authorization: Bearer $NODIUM_KEY"

Response

200 OK
{ "data": { "items": [ {} ], "nextCursor": "…", "hasMore": true }}

Send a message

POST/api/v1/messages

Sends a message from one of your WhatsApp numbers to one person. You name the number (channelId) and the person (to, or recipient); Nodium finds their conversation, or opens it, and sends. Everything goes through this one call, chosen by type: free text, an interactive form (quick-reply buttons, a list, a location request, a WhatsApp Flow), a location pin, a contact card (contacts), an approved template, a file (image, video, audio, document), or a reaction. Fields that do not belong to the type are refused.

The person. to is a phone number in international format (E.164, for example +33612345678). A contact who does not expose a number is written to by recipient, their WhatsApp user id (wa:FR.13491208655302741918, as found in GET /contacts). Give one of the two, not both. Someone Nodium has never seen becomes a contact and gets a conversation on the spot.

The 24-hour window is enforced here. On WhatsApp you may only send free-form messages (every type except template and reaction) within 24 hours of the person's last message. Outside it, and to anyone who has never written to you, you get window_closed (409) and must send a template instead. Interactive forms count as free text for Meta.

Files. Upload the file first with POST /media, which returns a mediaId valid for 7 days, then send it here with type set to its kind. The same mediaId can be sent to many people.

Idempotency. An idempotency key is required on every call: the idempotencyKey field, or the Idempotency-Key request header (same value rules: 8 to 200 characters). 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 and returns the first message with duplicate: true, even if the window has closed since. A retry must carry the same request (number, recipient, type and content): the same key with a different content is refused with 409 idempotency_key_conflict and nothing is sent. queue and scheduledAt are not part of that comparison. A message written before this check existed is returned as a duplicate without being compared.

Rate limit. Free-form messages are limited to 600 per minute per key and per client (429 with Retry-After); templates are not, they are governed by your plan's allowance.

The message is attributed to your key: the conversation shows the key's label, never a person who did nothing. With queue: true the call returns at once with the message queued (HTTP 202) and it leaves within a minute; follow it with the message.sent / message.failed webhooks. With scheduledAt the message is stored now with status scheduled (HTTP 202) and sent at that time instead; queue wins if you set both. A reaction is neither stored as a message nor queued: message is null in the answer.

A refused send is not an HTTP error. When the provider refuses the message, the call still answers 200 or 201 with message.status: "failed", message.error (the provider's reason) and message.errorInfo (what happened, what to do, who acts). Always test message.status, not only the HTTP code. The message.failed webhook carries the same.

Body

  • channelIdstringrequired

    Which number to speak from. See GET /channels.

  • tostring

    The person's phone number in E.164 form. This or recipient is required.

  • recipientstring

    Instead of to: the person's WhatsApp user id, wa: followed by the id, for a contact who does not expose a number.

  • idempotencyKeystring

    Your own key, 8 to 200 characters, unique per message. Sending twice with the same key sends once. Required unless you send it as the Idempotency-Key header instead.

  • typestringrequired

    What to send. Each type takes its own fields, below.

    textinteractivelocationcontactstemplateimagevideoaudiodocumentreaction

  • textstring

    For text (required, 4,096 characters at most): what to say. For interactive: the body of the form when form.body is absent.

  • formobject

    For interactive (required): up to 3 buttons, a list of up to 10 rows, a location request, one button that opens a web address (kind: "link"), or a WhatsApp Flow that opens on a button (kind: "flow").

    Show fields
    • kindstringrequired

      Which form.

      buttonslistlocation_requestlinkflow

    • bodystring

      The text above the form, 1024 characters at most. Defaults to text.

    • headerstring

      A line above the body, 60 characters at most.

    • footerstring

      A line below the body, 60 characters at most.

    • buttonsobject[]

      For buttons: 1 to 3. The reply you receive carries the chosen id.

      Show fields
      • idstring

        Your identifier, returned when tapped.

      • titlestringrequired

        20 characters at most.

    • buttonLabelstring

      For list, link and flow: the label of the button, 20 characters at most (required for link and flow).

    • urlstring

      For link (required): the address the button opens, https://…, 2,000 characters at most.

    • flowIdstring

      For flow (required): the id of the Flow at Meta (metaFlowId in GET /flows), 100 characters at most.

    • flowTokenstring

      For flow: your own token, 256 characters at most, returned as is with the answer (GET /flow-responses?flowToken=) to tie it to this send.

    • screenstring

      For flow: the screen to open on, when it is not the first.

    • dataobject

      For flow: values passed to the first screen.

    • actionstring

      For a dynamic flow: open it with an INIT call to its data endpoint, which returns the first screen and its data. Not with screen or data.

      data_exchange

    • modestring

      For flow: draft to try a Flow that is not published yet. Default published.

      publisheddraft

    • sectionsobject[]

      For list: sections of rows, 10 rows in all.

      Show fields
      • titlestring

        The section title.

      • rowsobject[]

        Its rows.

        Show fields
        • idstring

          Your identifier, returned when chosen.

        • titlestringrequired

          24 characters at most.

        • descriptionstring

          72 characters at most.

  • locationobject

    For location (required): a location pin.

    Show fields
    • latitudenumberrequired
    • longitudenumberrequired
    • namestring
    • addressstring
  • contactsobject[]

    For contacts (required): contact cards. Meta limits to 1 per message.

    Show fields
    • nameobjectrequired
    • phonesarray
    • emailsarray
  • templateNamestring

    For template: the template, by its name. This or templateId is required.

  • templateLanguagestring

    For template, with templateName: its language code. Default fr.

  • templateIdstring

    For template: the template, by its id, instead of templateName.

  • variablesobject

    For template: the values filling its placeholders: by number for {{1}} ({ "1": "Karim" }), by name for {{client_name}} ({ "client_name": "Karim" }).

  • headerMediaIdstring

    For a template with an image, video or document header: the file to show in it, a mediaId from POST /media of the same kind. Omitted, the template's own header sample is sent.

  • buttonsobject[]

    For template: the values of the template's dynamic buttons: { index, type, … } — index is the button's position in the template (from 0), type is url (with text, the link suffix filling {{1}}), quick_reply (with payload), copy_code (with code, 15 characters at most) or flow (optional flowToken, returned with the answer, and data, the values of the first screen). A link button with {{1}} and a copy-code button must be given. A template with a Flow button opens the form outside the 24-hour window.

  • mediaIdstring

    For image, video, audio, document (required): the id returned by POST /media. Its kind must match type.

  • captionstring

    For a file: a caption shown under it, 1,024 characters at most. WhatsApp shows none under audio.

  • voiceboolean

    For audio: true to send it as a voice note, with its waveform (the file must be audio/ogg).

  • messageIdstring

    For reaction (required): the Nodium id of the message to react to, which must belong to this person's conversation on this channel.

  • emojistring

    For reaction (required): the emoji, or "" to remove the last reaction you sent.

  • replyToIdstring

    Quote an earlier message of this person's conversation (its Nodium id). Not for a reaction.

  • queueboolean

    Return at once and send within a minute (HTTP 202). Default false.

  • scheduledAtstring

    ISO 8601 date-time. The message is stored now and sent at that time. Must be at least 1 minute in the future.

Response

The message as stored, with its delivery status, and the id of the conversation it belongs to — new or reused.

  • data.conversationIdstring

    The conversation the message belongs to — opened by this call, or reused.

  • data.messageMessage
  • data.duplicateboolean

    true when this idempotencyKey was already used: the message returned is the first one, nothing was sent again.

Refusals

  • 404unknown_channel`channelId` is not one of this client's numbers.
  • 409channel_inactiveThis number is switched off: switch it on before sending from it.
  • 409window_closedMore than 24 hours since the person last wrote, or they never did. Send an approved `template` instead.
  • 409contact_blockedThis contact is blocked: nothing is sent to them.
  • 409consent_missingA marketing template to a contact who opted out.
  • 409template_not_approvedThe template is not approved by Meta at this client yet; `data.status` says where it stands there (`not_deployed`, `submitted`, `rejected`…).
  • 402quota_exceededYour plan's monthly allowance is used up; `data.resetsAt` says when sends resume.
  • 404media_not_found`mediaId` is unknown, belongs to another client, or has expired (7 days): upload the file again.
  • 404message_not_foundThe message to react to, or to quote, is not in this person's conversation.
  • 422invalid_type`type` is missing or not one of the listed values.
  • 422invalid_fieldA field was given that does not belong to this `type`.
  • 422recipient_requiredNeither `to` nor `recipient` was given.
  • 422recipient_invalid`to` is not a number in international format, `recipient` is not a `wa:…` id, or both were given.
  • 422no_recipientThis contact has no address to write to: neither a phone number nor a WhatsApp user id (`wa:…`).
  • 422text_too_longThe text is over 4,096 characters, the WhatsApp limit. Nothing is stored; `limit` says the maximum.
  • 422caption_too_longThe caption is over 1,024 characters, the WhatsApp limit.
  • 422caption_unsupportedWhatsApp shows no caption under audio — send the text separately.
  • 422media_type_mismatchThe file behind `mediaId` is not of the kind named by `type`.
  • 422idempotency_key_requiredThe idempotency key (`idempotencyKey` field or `Idempotency-Key` header) is missing or outside 8-200 characters.
  • 422invalid_requestAmong others: the `Idempotency-Key` header and the `idempotencyKey` field are both given and differ; `detail` says which.
  • 409idempotency_key_conflictThat key already served a request with different content (number, recipient, type, text, template values…). Retry with the exact same request, or use a new key; nothing is sent.
  • 422invalid_formThe form breaks a WhatsApp limit; `detail` says which.
  • 422invalid_locationThe location is missing or its coordinates are out of range.
  • 422invalid_contactsThe contact cards are missing, empty, or over the limit of 1.
  • 422template_holes_unfilledA placeholder has no value; `data.issues` lists them.
  • 422template_header_invalid`headerMediaId` was given for a template without a media header, or the template's header sample is missing.
  • 422template_buttons_invalidA button value does not match the template's buttons; `detail` says which.
  • 422scheduled_at_invalid`scheduledAt` is not a valid ISO date, or is less than a minute away.
  • 422queue_not_supportedA reaction cannot be quoted, queued or scheduled.
  • 429rate_limitedMore than 30 free-form messages in a minute for this key and client; wait `Retry-After` seconds.
curl -X POST https://nodium.io/api/v1/messages \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "channelId": "…", "to": "+33612345678", "recipient": "wa:FR.13491208655302741918", "type": "text", "templateLanguage": "fr" }'

Response

200 OK
{ "data": { "conversationId": "…", "message": {}, "duplicate": true }}

Get one message

GET/api/v1/messages/{id}

Returns one message, sent or received, as it stands now. Use it to follow a message after sending it without listing its conversation: status moves from queued or sending to sent, delivered, read, or failed (the send call itself can answer 200 or 201 with status: "failed"). When it failed with a provider code, errorInfo says what happened, what to do and who acts. The webhooks (message.sent, message.delivered, message.read, message.failed) tell you the same without polling.

Path parameters

  • idstring

    The message id, as returned when you sent it or listed its conversation.

Response

The message, with its current delivery status and, when it failed, `errorInfo`.

  • data.idstring

    The message id.

  • data.conversationIdstring

    Its conversation.

  • data.channelTypestring

    The kind of channel.

    whatsapp

  • data.directionstring

    inbound from the contact, outbound to them.

    inboundoutbound

  • data.senderstring

    Who speaks: the contact, your software (api, through a key), a person in the console (user), or Nodium (system).

    contactapiusersystem

  • data.senderNamestring

    The person's name, or your key's label.

  • data.apiKeyIdstring

    The key that sent it, when it went through the API.

  • data.typestring

    text, template, interactive, image, audio, document…

  • data.textContentstring

    Its text. For a template, the rendered text.

  • data.mediaUrlstring

    Where to download its file (GET /messages/{id}/media), or null.

  • data.statusstring

    Delivery status. queued: waiting to leave (queue: true).

    queuedsendingsentdeliveredreadfailed

  • data.providerMessageIdstring

    The id at the messaging provider (WhatsApp wamid), or null.

  • data.errorstring

    Why it failed, as the provider said, or null.

  • data.errorInfoobject

    The failure explained, when status is failed and the provider gave a code; otherwise null. A send can answer HTTP 200 or 201 with status: "failed": test status, then read this.

    Show fields
    • codeinteger

      The provider's error code (Meta's, for example 131047).

    • titlestring

      What happened, in a sentence.

    • actionstring

      What to do about it. When actor is client, written to be passed on to your client as is.

    • actorstring

      Who acts: you (your code or console), client (the owner of the number), nodium, or wait (the refusal is temporary: nobody needs to act).

      youclientnodiumwait

  • data.formobject

    The buttons or list sent with it, or null.

  • data.replyobject

    For a tapped button or chosen list row: { id, title }. Otherwise null.

  • data.locationobject

    A location the contact sent: { latitude, longitude, name, address }, or null.

  • data.originstring

    phone when the message was written from the WhatsApp Business app of a coexistence number, otherwise null.

  • data.importedboolean

    True for a message taken from the phone's chat history (coexistence), not received live.

  • data.createdAtstring

    ISO 8601.

Refusals

  • 404message_not_foundThe id is unknown, malformed, or belongs to another client or account.
curl https://nodium.io/api/v1/messages/<id> \ -H "Authorization: Bearer $NODIUM_KEY"

Response

200 OK
{ "data": { "id": "…", "conversationId": "…", "channelType": "whatsapp", "direction": "inbound", "sender": "contact", "senderName": "…", "apiKeyId": "…", "type": "…", "textContent": "…", "mediaUrl": "…", "status": "queued", "providerMessageId": "…", "error": "…", "errorInfo": { "code": 0, "title": "…", "action": "…", "actor": "you" }, "form": {}, "reply": {}, "location": {}, "origin": "…", "imported": true, "createdAt": "…" }}

Download the file attached to a message

GET/api/v1/messages/{id}/media

Returns the file itself. Nodium keeps its own copy: Meta deletes received media after seven days, so this stays available afterwards.

Path parameters

  • idstring

    The message id.

Response

The file, with its content type.

curl https://nodium.io/api/v1/messages/<id>/media \ -H "Authorization: Bearer $NODIUM_KEY"

Returns the file itself, not wrapped: its type is in Content-Type.