Triggers and the API
Every flow has exactly one trigger — the trigger object in its definition. You pick and configure it in the Trigger step of the flow-creation wizard and can change or fine-tune it anytime on the canvas.
| Trigger kind | When it fires |
|---|---|
manual | Started explicitly — via POST /flows/{id}/runs (the most common integration pattern), from the Scheduler, or by a team member clicking "Start Flow" on a contact's page. |
incoming_message | Any inbound message from a contact (optionally restricted to specific channels). |
keyword_match | An inbound message matches configured keywords — exact, contains, or regex match modes, with optional case sensitivity. |
conversation_opened | A new conversation is opened on any channel. |
conversation_closed | A conversation transitions to closed — by an agent, a flow, or an auto-close rule. |
contact_tag_added | A specific tag is added to a contact. |
comment_received | An Instagram comment on your posts matches the configured filters (requires picking the Instagram account to watch). |
click_to_ad | The first inbound message of an ad-originated conversation (WhatsApp CTWA, Messenger CTM, Instagram click-to-ad), optionally narrowed to specific ads. |
schedule | Cron-style or birthday-anchored. Runs evaluate every minute via the in-pod rule evaluator. |
api_event | Internal platform events (e.g. contact.created) start the flow. |
This guide focuses on the manual API path — the way external systems integrate.
Conversation lifecycle triggers
Flows can subscribe to two conversation events:
| Trigger | When it fires |
|---|---|
conversation_opened | A new conversation is opened (any channel, including widget live chat, WhatsApp first-message, etc.). |
conversation_closed | A conversation transitions to the closed state — whether by an agent, a flow's Open Conversation follow-up, or an auto-close rule. Useful for NPS surveys, auto-tagging closed tickets, and follow-up sequences. |
Both share the same config surface:
- Channel allow-list — restrict the trigger to specific channel IDs (e.g., only fire on your DE WhatsApp).
- Contact filters — only fire when the contact matches a set of tag / list / custom-field rules.
Configure these on the flow's Trigger tab.
Endpoints
POST /api/v1/flows # create
GET /api/v1/flows # list
GET /api/v1/flows/{id} # read
PATCH /api/v1/flows/{id} # update definition
POST /api/v1/flows/{id}/publish # draft → published
POST /api/v1/flows/{id}/pause # published → paused
DELETE /api/v1/flows/{id} # archive
POST /api/v1/flows/{id}/runs # start a run for a contact
GET /api/v1/flows/{id}/runs # list runs (filter by contact_id, status)
GET /api/v1/flows/{id}/runs/{run_id} # run detail with step audit
POST /api/v1/flows/{id}/runs/{run_id}/stop # cancel an in-flight run
GET /api/v1/flows/variables?flow_id={uuid} # variables available inside a flow
Variables endpoint
GET /api/v1/flows/variables?flow_id={uuid} returns the typed list of values the Branch-node variable picker (and other condition UIs) can reference:
{
"contact_fields": ["first_name", "last_name", "email", "phone", "language", "..."],
"custom_fields": ["loyalty_tier", "order_count", "..."],
"tags": ["vip", "trial", "..."],
"lists": ["newsletter", "active-customers", "..."],
"flow_variables": ["captured_email", "kb_answer", "tracked_link_url", "..."]
}
flow_variables are sourced from the flow definition itself — specifically, the output_var_name field on collect_input nodes, output_var on kb_query nodes, and output_var_name on create_tracked_link nodes. This means the picker only ever surfaces variables that actually exist downstream of a given node.
All endpoints require a Bearer API token with the flows:* scopes shown in the Overview.
Starting a run
The simplest case — start a flow for a known contact:
curl -X POST "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "08621525-6836-4578-bf43-4145d8b2097e"
}'
Response:
{
"id": "488a51ac-80a5-41a2-b5f5-425846394201",
"flow_id": "5b5a7f31-aeb1-4cfb-b747-b698303833fe",
"contact_id": "08621525-6836-4578-bf43-4145d8b2097e",
"status": "running",
"current_node_id": "node_welcome",
"started_at": "2026-04-29T10:44:09.234567Z"
}
The first node executes synchronously inside the request handler when possible (Send and Tag nodes are non-blocking). Long-running nodes (Delay, Collect Input, Condition waits) immediately return a running or waiting response and continue in the background.
Passing data into the run
Use trigger_payload to seed flow variables that the run can reference via {{vars.<key>}} or {{flow.var.<key>}}:
curl -X POST "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "08621525-6836-4578-bf43-4145d8b2097e",
"trigger_payload": {
"order_id": "ORD-12345",
"order_total": "49.99",
"support_link": "https://example.com/support/ORD-12345"
}
}'
Inside the flow's nodes, {{vars.order_id}} resolves to ORD-12345.
Idempotency
Pass idempotency_key to make repeated POSTs safe — duplicate keys return the existing run instead of creating a new one:
{
"contact_id": "08621525-6836-4578-bf43-4145d8b2097e",
"idempotency_key": "order-confirmation:ORD-12345"
}
This is critical when wiring flows to your own webhook handlers (Stripe, your CRM, etc.) — retries from those systems would otherwise spawn duplicate runs.
Re-entry rules
If the contact already has an active run on this flow, the engine applies the flow's re_entry_policy:
block— the request returns409 Conflictwith{ "code": "flow_already_running", "existing_run_id": "..." }. No new run is created.restart— the existing run is canceled, a new run starts. Returns201 Createdwith the new run ID.parallel— both runs execute side by side. Returns201 Createdfor the new run.
Listing and filtering runs
# All runs for a flow
curl "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs?status=running&limit=50" \
-H "Authorization: Bearer $API_TOKEN"
# Runs for a specific contact
curl "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs?contact_id=$CONTACT_ID" \
-H "Authorization: Bearer $API_TOKEN"
Each run includes the audit trail when fetched by ID:
curl "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs/$RUN_ID" \
-H "Authorization: Bearer $API_TOKEN"
The response includes steps[] — every node execution with started_at, finished_at, status, output_snapshot, and error_text (if any). Timestamps are full microsecond ISO strings.
Stopping a run
curl -X POST "https://api.sendseven.com/api/v1/flows/$FLOW_ID/runs/$RUN_ID/stop" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "customer-requested-cancellation" }'
The run transitions to canceled, any active waits are cleaned up, and a flow.run.failed webhook fires with { reason: "stopped" }.
Webhooks
Subscribe to flow events through the normal webhooks setup:
{
"event": "flow.run.completed",
"data": {
"tenant_id": "...",
"flow_id": "...",
"flow_name": "Welcome series",
"run_id": "...",
"contact_id": "...",
"started_at": "2026-04-29T10:44:09.234567Z",
"completed_at": "2026-04-29T10:48:33.781290Z",
"steps_executed": 7,
"final_node_id": "node_followup_email"
}
}
Available event types:
flow.run.started— fires immediately afterPOST /flows/{id}/runssucceeds.flow.run.completed— fires when the run reaches a terminal node.flow.run.failed— fires onfailedorcanceledruns.
Error responses
| Code | Meaning |
|---|---|
400 invalid_definition | The flow's definition is malformed (orphan nodes, missing edge branches, etc.). Returned from POST /publish. |
404 flow_not_found | The flow doesn't exist or belongs to another tenant. |
404 contact_not_found | The contact ID doesn't exist in your workspace. |
409 flow_already_running | Re-entry policy is block and an active run exists. |
409 flow_not_published | Cannot start runs on draft, paused, or archived flows. |
422 invalid_trigger_payload | trigger_payload is not a flat JSON object. |
429 rate_limit_exceeded | Per-tenant rate limit on Send-node fan-out. |