Skip to content
Get an API key

Get started

Flows

WhatsApp forms: draw them in JSON, publish them at Meta, open them from a button in the conversation.

A Flow is an interactive form that opens inside WhatsApp: several screens, fields, choices. It is drawn as JSON (Meta's Flow JSON), lives at Meta, and is opened by a button you send in a conversation. Nodium manages its life cycle and reads the answers; what the form does is yours — the JSON says it, or your own endpoint does.

  • Flows belong to a client: they live in one of its WhatsApp Business accounts, so the client needs a connected number (no_channel otherwise). If it has several accounts, name one with wabaId.
  • Reading (GET) is open to every role; creating, changing, publishing and deleting need admin.
  • status: draft (editable), published (sendable, final), deprecated, blocked or throttled (Meta flags an unhealthy endpoint). mode: static, or dynamic once a data endpoint is attached.

Shell

# 1. Create a draft (without flowJson it starts from a small survey)curl https://nodium.io/api/v1/flows \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Booking", "categories": ["APPOINTMENT_BOOKING"] }'# → data.flow.id, data.flow.metaFlowId, data.validationErrors# 2. Save your own JSON as a new version; Meta validates itcurl https://nodium.io/api/v1/flows/<flow id>/versions \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "flowJson": { "version": "7.2", "screens": [ … ] } }'# 3. Look at it as on a phone, then publish (final)curl https://nodium.io/api/v1/flows/<flow id>/preview -H "Authorization: Bearer $NODIUM_KEY"curl -X POST https://nodium.io/api/v1/flows/<flow id>/publish -H "Authorization: Bearer $NODIUM_KEY"# 4. Send it: a button in the conversation opens the form (inside the 24-hour window)curl https://nodium.io/api/v1/messages \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "channelId": "<channel id>", "to": "+33612345678", "type": "interactive", "text": "Pick a slot for your visit.", "idempotencyKey": "visit-1042-flow", "form": { "kind": "flow", "flowId": "<data.flow.metaFlowId>", "buttonLabel": "Book", "flowToken": "visit-1042" } }'# 5. The answer comes back as message.received (data.reply.kind = "flow"),# and stays readable with your token:curl "https://nodium.io/api/v1/flow-responses?flowToken=visit-1042" \ -H "Authorization: Bearer $NODIUM_KEY"

Create, version, preview, publish

  • POST /flows creates a draft at Meta with its first version. Without flowJson it starts from a small satisfaction survey. categories are Meta's (OTHER by default). POST /flows/sync also pulls in Flows drawn directly in WhatsApp Manager.
  • POST /flows/{id}/versions saves a JSON (10 MB at most) as a new version, which Meta validates. Invalid JSON is saved too: validationErrors says what to fix. The same JSON as the latest version opens no new one. GET /flows/{id}/versions lists them (v1, v2…, without the JSON); GET /flows/{id}/versions/{versionId} returns one with its JSON.
  • GET /flows/{id}/preview returns the address of the page Meta renders — the form as on a phone, Android or iOS, light or dark. It expires after about thirty days; ask again.
  • POST /flows/{id}/publish is final: a published Flow can no longer be changed or unpublished. Meta refuses JSON that still has validation errors, and says why (502 upstream_error).
  • POST /flows/{id}/duplicate copies a Flow into a new draft: the way to change a published one. PATCH /flows/{id} renames it or changes its categories, even when published. POST /flows/{id}/deprecate retires a published Flow; DELETE /flows/{id} removes a draft only.

Send a Flow

In POST /messages, with type: "interactive", pass form.kind: "flow" with the Flow's flowId (its metaFlowId) and a buttonLabel (20 characters at most). Optional: flowToken (your own token, returned with the answer), screen (a first screen other than the default), data (values for that screen) and mode: "draft" to try an unpublished Flow. Like every form, it counts as free text: the 24-hour window applies.

Read the answers

  • When the contact submits, you receive message.received with data.reply of kind flow: data.reply.data holds what they filled in, field by field.
  • GET /flow-responses lists the answers, most recent first, with flowToken, from and to filters and cursor pagination. flowToken fetches the answer to one particular send.
  • The answers are read from the received messages, so they are kept as long as message content is (your plan's retention). Store what you need from the webhook.

A dynamic Flow: your own endpoint

A Flow can ask your server what to show on each screen (available slots, a price, a lookup). Attach your https address to a draft: Meta calls Nodium on every screen, Nodium decrypts the encrypted request and relays it to you as a signed POST; your JSON answer is encrypted and handed back to Meta. Nodium carries; it decides nothing.

Shell

# Attach your address to a draft: the Flow becomes dynamiccurl -X PUT https://nodium.io/api/v1/flows/<flow id>/data-endpoint \ -H "Authorization: Bearer $NODIUM_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/whatsapp/flow" }'# → data.dataEndpoint.metaUrl, and data.secret ("whsec_…", shown once)
  • PUT /flows/{id}/data-endpoint needs a public https address (private addresses are refused, as for webhooks) and a draft. It also generates the client's encryption key and registers it on its numbers; unregisteredChannels lists the numbers that did not take it — call the route again once fixed.
  • The signing secret is returned once, by this call. Calling it again issues a new one, and the old one stops working at once. GET /flows/{id}/data-endpoint never returns it.
  • Your address receives the body below, signed like a webhook: verify X-Nodium-Signature (see Webhooks). You have about 9 seconds.
  • DELETE /flows/{id}/data-endpoint makes a draft static again. Duplicating a Flow does not copy the endpoint: attach it again on the copy.
  • GET /flows/{id}/invocations shows the last 100 calls of the last 30 days — action (INIT, data_exchange, BACK, ping), screen, the status your server answered, duration, error. The content exchanged is never kept.

HTTP

POST https://api.example.com/whatsapp/flowContent-Type: application/jsonX-Nodium-Signature: t=1790000000,v1=<hex>{ "source": "whatsapp_flow", "flow": "<flow id>", "data_exchange": { "version": "3.0", "action": "data_exchange", "screen": "SLOTS", "data": { "date": "2026-10-02" }, "flow_token": "visit-1042" }, "signature_valid": true, "received_at": "2026-09-29T10:00:00.000Z"}# You answer, within about 9 seconds:{ "screen": "CONFIRM", "data": { "slots": [ { "id": "10", "title": "10:00" } ] } }