Triggering a flow through the API
A flow is an automation built in the dashboard's visual editor: messages, questions, conditions, waits. The API trigger lets your system start that flow for a contact at the right moment — an order paid, a lead arriving from the website, an invoice overdue — without building each message in code.
Preparing the flow
- In the editor, add the API trigger and connect it to the first step.
- Optionally, pick on the trigger the number that will send.
- Publish and activate the flow. The trigger always uses the published version —
a trigger that only exists in the draft answers
422 flow_no_api_trigger.
The flow id shows in the editor and in GET /flows.
Triggering
curl -X POST https://api.joinotify.com/flows/cm1fl0w000001/trigger \
-H 'Authorization: Bearer sk_live_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1042-paid' \
-d '{
"contact": { "phone": "+5541987111527" },
"data": { "order": "1042", "total": "$ 189.90" }
}'{ "data": { "runId": "cm1run0000001", "replayed": false } }- The contact comes as
contact.idorcontact.phone. With the phone, a contact that does not exist is created — and firescontact.created. - The data in
datais available in the flow as{{trigger.order}}, with an optional default:{{trigger.coupon|no coupon}}. Nested data works too —{{trigger.customer.name}},{{trigger.items[0].name}}—, with formatting such as{{trigger.total:money}}: see Variables in flows. - The sending number is
phoneNumberId, if you send it; otherwise the one set on the trigger; otherwise the account's oldest active number.
The answer arrives before the messages: the flow runs right after, outside the request.
runId: null is not an error
It happens when the trigger is connected to no step, or when the same contact started this flow a few moments ago — there is a minimum interval between runs, so that a duplicate event in your system does not send everything twice.
Starting a run through the API cancels the contact's other live runs on that number, in any flow. If the platform that reports the order is not your own system, the webhook trigger takes its data directly, with no key — and, by default, runs alongside whatever the contact already has going on.
Retrying without duplicating
With Idempotency-Key, repeating the trigger within 24 hours returns 200 with the same
runId and replayed: true, without starting another run. Two differences from
POST /messages:
- the body is not compared — the same key returns the same run, even with different
data; - a call that failed (inactive flow, invalid number) does not keep the key, and the repeat tries again.
What is checked later
The trigger accepts the request; the sending rules apply when each message goes out:
- a marketing template to a contact who opted out or is on the suppression list ends
the run with the reason
contact_opted_out; - a free-form message with the 24-hour window closed ends with
window_closed, unless the step has a fallback template configured; - if the account loses its entitlement along the way, the run stops with
billing.
Following the outcome
Subscribe to two events on your webhook endpoint:
| Event | What it carries |
|---|---|
flow.answer.captured | Each answer a Question or Wait-for-reply step stored |
flow.run.completed | The end of the run, with every flow variable |
The data you sent in data does not come back in the event — keep the runId to match
both ends. Runs that fail or are cancelled produce no flow.run.completed. The payloads
are in the event catalog.
Limits and scope
- Each trigger spends one request from your key's sending limit. The messages the flow sends afterwards do not.
- An API key limited to some numbers can trigger, but only with numbers in its scope. Listing and reading flows needs an unrestricted key.