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}}. - 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 errorIt 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 cancels the contact's other live runs on that number, in any flow. A contact talks to one flow at a time.
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.