Skip to main content

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 kindWhen it fires
manualStarted 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_messageAny inbound message from a contact (optionally restricted to specific channels).
keyword_matchAn inbound message matches configured keywords — exact, contains, or regex match modes, with optional case sensitivity.
conversation_openedA new conversation is opened on any channel.
conversation_closedA conversation transitions to closed — by an agent, a flow, or an auto-close rule.
contact_tag_addedA specific tag is added to a contact.
comment_receivedAn Instagram comment on your posts matches the configured filters (requires picking the Instagram account to watch).
click_to_adThe first inbound message of an ad-originated conversation (WhatsApp CTWA, Messenger CTM, Instagram click-to-ad), optionally narrowed to specific ads.
scheduleCron-style or birthday-anchored. Runs evaluate every minute via the in-pod rule evaluator.
api_eventInternal 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:

TriggerWhen it fires
conversation_openedA new conversation is opened (any channel, including widget live chat, WhatsApp first-message, etc.).
conversation_closedA 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 returns 409 Conflict with { "code": "flow_already_running", "existing_run_id": "..." }. No new run is created.
  • restart — the existing run is canceled, a new run starts. Returns 201 Created with the new run ID.
  • parallel — both runs execute side by side. Returns 201 Created for 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 after POST /flows/{id}/runs succeeds.
  • flow.run.completed — fires when the run reaches a terminal node.
  • flow.run.failed — fires on failed or canceled runs.

Error responses

CodeMeaning
400 invalid_definitionThe flow's definition is malformed (orphan nodes, missing edge branches, etc.). Returned from POST /publish.
404 flow_not_foundThe flow doesn't exist or belongs to another tenant.
404 contact_not_foundThe contact ID doesn't exist in your workspace.
409 flow_already_runningRe-entry policy is block and an active run exists.
409 flow_not_publishedCannot start runs on draft, paused, or archived flows.
422 invalid_trigger_payloadtrigger_payload is not a flat JSON object.
429 rate_limit_exceededPer-tenant rate limit on Send-node fan-out.