Messaging

Flows

Interactive WhatsApp forms hosted by Meta. Store the JSON definition with Tyxter, publish to Meta, and receive every end-user submission as a signed webhook.

Create

POST/v1/flows
curl https://api.tyxter.com/v1/flows \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "lead_capture",
    "flow_json": {
      "version": "6.3",
      "screens": [
        {
          "id": "WELCOME",
          "title": "Welcome",
          "terminal": true,
          "layout": { "type": "SingleColumnLayout", "children": [] }
        }
      ]
    }
  }'

We validate the JSON structurally before storage — unsupported versions, missing screens, missing terminal screens, and duplicate screen ids are rejected with 400 invalid_flow_json. Meta will do its own semantic validation at publish time.

Send

POST/v1/messages

Send a published flow to a recipient with the channel-native messages route, using message.type = "flow". Address the flow with flow.action.parameters.flow_id — the published flow's provider_flow_id, which you read from GET /v1/flows/{id} once publish completes — or with flow.action.parameters.flow_name; provide exactly one. flow_token is your required opaque correlation token: it comes back to you as submission.provider_token on the flow.completed webhook, so use it to tie a completion back to your own record. flow_cta is the required button label (max 30 chars).

curl https://api.tyxter.com/v1/messages \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "channel": "whatsapp",
    "sender": { "type": "whatsapp_phone_number", "id": "pn_..." },
    "recipient": { "type": "phone_e164", "id": "+5511999999999" },
    "message": {
      "type": "flow",
      "flow": {
        "body": { "text": "Tap below to complete your booking." },
        "action": {
          "name": "flow",
          "parameters": {
            "flow_id": "1234567890",
            "flow_token": "booking-7f3a",
            "flow_cta": "Book now"
          }
        }
      }
    }
  }'

Publish

POST/v1/flows/{id}/publish

Snapshots the current flow_json into a new FlowVersion and enqueues the Meta sync worker. Returns 202.

Business verification is a confirmed common gate for publishing Flows: the Meta Business Manager (business portfolio) that owns your WhatsApp Business Account must complete it. A payment method does not substitute for business verification. A numeric 139000 rejection is account-scoped; Tyxter may append Account health entries with their numeric code plus any provider-supplied entity, description, or remediation. See Troubleshooting.
Sandbox auto-publishes without hitting Meta so you can build the submission → webhook loop locally.

Troubleshooting

A rejected publish archives the flow. For a Meta-originated rejection, rejection_reasonpreserves Meta's inline explanation and may add the account-health context described below; it is readable on GET /v1/flows/{id} and attached to the flow.rejected webhook. rejection_reason is diagnostic free text that may contain platform text, the inline Meta explanation, and Tyxter-formatted provider health context; branch on Flow status and never parse the string. The common rejections:

Account-integrity rejection: 139000

Meta can return Integrity requirements not met. with numeric Graph code 139000. Numeric Meta error 139000 is account-scoped, not a single diagnosis. When Meta exposes Account health entries, Tyxter appends their numeric code plus any provider-supplied entity, description, or remediation to rejection_reason. Follow whichever provider fields are present.

Business verification is a confirmed common Flow-publish gate, and a payment method does not substitute for it; a WABA 141006 payment-method health issue can be adjacent business-initiated messaging or registration context rather than the Flow-publish cause. For example, a provider-supplied BUSINESS (141010) description or remediation can direct you to Meta Business Suite under Settings → Business verification. A rejected or archived Flow is terminal for customer-initiated publish and retry paths; fix the Meta account state, then create or clone and publish a fresh Flow instead of relying on an in-place retry.

Validation errors: …

Meta's semantic validation rejected the flow body. We validate structure before storage, but Meta applies deeper rules at publish time; when it does, the rejection reason appends up to five of Meta's specific complaints, for example Required property '__example__' is missing. or Screen "SUMMARY" is unreachable. Fix the flow_json in a new flow (or dashboard clone) and publish that one.

provider_missing_since is set

Meta no longer resolves the stored provider_flow_id. Tyxter preserves the last known lifecycle status because a temporary permission problem can look like a deleted Flow, records the first observed time in provider_missing_since, and clears the marker after a later successful provider read. Check the Flow in Meta Business Suite and reconnect the WhatsApp account if the Flow should still exist.

Republish returns 409 flow_archived

An archived Flow cannot be re-published through the customer API. Fix the Meta account state, then create a new Flow with the corrected flow_json — or clone in the dashboard, which resets the provider state — and publish the fresh Flow instead of relying on an in-place retry.

Completions

In production, Meta sends an end-user's reply in its ordinary messages callback as interactive.nfm_reply, not in a Flow lifecycle callback. Tyxter preserves that ordinary inbound message and independently derives a FlowSubmission (idempotent on flow_token) plus the same signed flow.completed webhook for your endpoints. The inbound-message and completion paths use separate queues, so do not rely on durable processing or customer delivery order between message.received and flow.completed.

{
  "id": "evt_...",
  "type": "flow.completed",
  "data": {
    "flow_id": "flw_...",
    "name": "lead_capture",
    "status": "published",
    "provider_flow_id": "1234567890",
    "rejection_reason": null,
    "submission": {
      "id": "sub_...",
      "provider_token": "flow-token-xyz",
      "response": { "email": "user_at_example_dot_com", "size": "M" },
      "message_id": "msg_...",
      "received_at": "2026-04-27T12:05:00.000Z"
    }
  }
}

Every flow.completed data carries the correlation fields you need to tie a submission back: flow_id (the Tyxter flw_... id), provider_flow_id (the Meta flow id the message was sent with), submission.id, submission.provider_token (the flow_token you sent), submission.response (the submitted fields), and submission.message_id. The parsed response never includes a top-level flow_token; use submission.provider_token and the source submission.message_id to correlate the completion.

Lifecycle events