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_channelotherwise). If it has several accounts, name one withwabaId. - Reading (
GET) is open to every role; creating, changing, publishing and deleting needadmin. status:draft(editable),published(sendable, final),deprecated,blockedorthrottled(Meta flags an unhealthy endpoint).mode:static, ordynamiconce a data endpoint is attached.
# 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 /flowscreates a draft at Meta with its first version. WithoutflowJsonit starts from a small satisfaction survey.categoriesare Meta's (OTHERby default).POST /flows/syncalso pulls in Flows drawn directly in WhatsApp Manager.POST /flows/{id}/versionssaves a JSON (10 MB at most) as a new version, which Meta validates. Invalid JSON is saved too:validationErrorssays what to fix. The same JSON as the latest version opens no new one.GET /flows/{id}/versionslists them (v1,v2…, without the JSON);GET /flows/{id}/versions/{versionId}returns one with its JSON.GET /flows/{id}/previewreturns 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}/publishis 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}/duplicatecopies 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}/deprecateretires 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.receivedwithdata.replyof kindflow:data.reply.dataholds what they filled in, field by field. GET /flow-responseslists the answers, most recent first, withflowToken,fromandtofilters and cursor pagination.flowTokenfetches 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.
# 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-endpointneeds 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;unregisteredChannelslists 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-endpointnever 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-endpointmakes a draft static again. Duplicating a Flow does not copy the endpoint: attach it again on the copy.GET /flows/{id}/invocationsshows 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.
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" } ] } }