Sequences
Automated flows, and enrolling contacts into them.
The Sequence object 12 fields
false (default) = a contact enrolls at most once ever; true = re-entry allowed once no longer actively enrolled.
Single-sequence GET only. Graph edges (id, sequence_id, from_node, to_node, branch_label, created_at); branch_label is null for an unlabelled edge, true/false on a condition node.
The graph node traversal starts at; null on an unconfigured draft.
What auto-enrolls contacts ({ trigger_type, trigger_config }), or null. A DSL object — shape varies by trigger type.
Global early-exit rules, or null. A DSL object.
Canonical sequence ID. Pass it to POST /{id}/enroll.
Single-sequence GET only. Typed graph nodes (id, sequence_id, type, config, created_at); config shape varies by node type.
Only an active sequence is enrollable — a draft or paused one refuses enrolment with SEQUENCE_NOT_ENROLLABLE.
draft, active, archived, pausedList sequences
The workspace's sequences with their status. Filter by status and substring-match the name with q. Use it to resolve a sequence id at startup instead of hard-coding one — a rebuilt flow gets a new id, and a hard-coded one would then fail exactly when you'd want it to. Rows are the bare sequence (no graph). Not paginated.
A page of Sequence objects, under data.
The workspace's sequences.
The request failed validation, or a referenced record was rejected.
The tenant slug, or the record the path names, does not exist.
curl -X GET "https://api.sonarsend.com/t/acme/api/sequences" \ -H "X-API-Key: $SONARSEND_API_KEY"
const res = await fetch('https://api.sonarsend.com/t/acme/api/sequences', {
method: 'GET',
headers: { 'X-API-Key': process.env.SONARSEND_API_KEY },
});
const data = await res.json();{
"data": [
{
"id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"name": "Acme onboarding",
"status": "draft",
"entry_node_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"entry_trigger": {}
}
],
"next_cursor": "eyJpZCI6IjljMjE4OTVkIn0",
"has_more": true
}Get a sequence
One sequence with its full node and edge graph. Read-only, and useful mostly for confirming you are pointed at the flow you think you are — the graph is authored in the app, not over the API.
The Sequence object.
The sequence with its graph.
The request failed validation, or a referenced record was rejected.
The tenant slug, or the record the path names, does not exist.
curl -X GET "https://api.sonarsend.com/t/acme/api/sequences/9c21895d-57f0-4a15-a1df-4dc6835d4f80" \ -H "X-API-Key: $SONARSEND_API_KEY"
const res = await fetch('https://api.sonarsend.com/t/acme/api/sequences/9c21895d-57f0-4a15-a1df-4dc6835d4f80', {
method: 'GET',
headers: { 'X-API-Key': process.env.SONARSEND_API_KEY },
});
const data = await res.json();{
"id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"tenant_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"name": "Acme onboarding",
"status": "draft",
"entry_node_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"entry_trigger": {}
}Enroll a contact
Enrolls a contact into an active sequence, resolving-or-creating the contact from the identifier(s) you send. At least one of contact_id, email, external_id is required (400 MISSING_IDENTIFIER otherwise); they are tried in that order of precedence.
contact_id— an exact lookup. No match is404 CONTACT_NOT_FOUND; it never creates.email— resolve-or-create. A new contact is created if none matches, carrying any profile fields you send (first_name,last_name,company,custom_fields,tags,stage_id) and, if you also sentexternal_id, that value stamped on it — so a signup webhook can enrol someone who does not exist yet in one call.external_idalone — a lookup only. It cannot create a contact, because a contact needs an email; no match is404 CONTACT_NOT_FOUND. Send it together withemailwhen you want create-if-missing.
Because of the precedence, sending contact_id (or email) together with external_id is fine — the higher one resolves the contact, the external_id is persisted. Unknown fields are rejected (400 INVALID_INPUT).
Idempotent: a contact who is already actively enrolled returns status: "duplicate" with 200 — a clean no-op. Pass an idempotency_key for at-least-once delivery; a retry with the same key is a no-op even if the first response never reached you.
contact_created on the response tells you whether this call created the contact or matched one. A draft or paused sequence refuses with 422 SEQUENCE_NOT_ENROLLABLE; the per-contact enrollment rate limit is 429 ENROLL_LIMIT (distinct from the per-key request limit).
enrolled, duplicate, sequence_not_active, enroll_limitEnrolled (enrolled) or an idempotent no-op (duplicate). contact_id and contact_created are present on the API-key resolve-or-create path.
The request failed validation, or a referenced record was rejected.
The tenant slug, or the record the path names, does not exist.
curl -X POST "https://api.sonarsend.com/t/acme/api/sequences/9c21895d-57f0-4a15-a1df-4dc6835d4f80/enroll" \
-H "X-API-Key: $SONARSEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"email": "jamie@example.com",
"external_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"idempotency_key": "welcome-series",
"first_name": "Jamie",
"last_name": "Reed"
}'const res = await fetch('https://api.sonarsend.com/t/acme/api/sequences/9c21895d-57f0-4a15-a1df-4dc6835d4f80/enroll', {
method: 'POST',
headers: {
'X-API-Key': process.env.SONARSEND_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"contact_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"email": "jamie@example.com",
"external_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"idempotency_key": "welcome-series",
"first_name": "Jamie",
"last_name": "Reed"
}),
});
const data = await res.json();{
"status": "enrolled",
"enrollment_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"contact_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"contact_created": true
}List a contact's enrollments
Every enrollment for one contact across ALL the workspace's sequences — the "did my enroll land, and where is it now" endpoint. A contact with no enrollments returns an empty data array, not a 404. Not paginated.
The graph node the contact is currently at; null once exited.
Why it ended; null while active.
When the enrollment ended; null while active.
Coarse classification derived from status + exit reason.
Enrollment status, e.g. active, completed, exited, paused.
Opaque. Pass back as cursor for the next page. Null when there is no continuation — which for most endpoints means the last page, and always means a collection that is not paginated. The account-wide event feed is the exception: it returns a resumable cursor even on its final page (see that endpoint).
The contact's enrollments across all sequences.
The request failed validation, or a referenced record was rejected.
The tenant slug, or the record the path names, does not exist.
curl -X GET "https://api.sonarsend.com/t/acme/api/sequences/contact/{contactId}/enrollments" \
-H "X-API-Key: $SONARSEND_API_KEY"const res = await fetch('https://api.sonarsend.com/t/acme/api/sequences/contact/{contactId}/enrollments', {
method: 'GET',
headers: { 'X-API-Key': process.env.SONARSEND_API_KEY },
});
const data = await res.json();{
"data": [
{
"enrollment_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"sequence_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80",
"sequence_name": "Acme onboarding",
"status": "active",
"outcome": "string",
"current_node_id": "9c21895d-57f0-4a15-a1df-4dc6835d4f80"
}
],
"next_cursor": "eyJpZCI6IjljMjE4OTVkIn0",
"has_more": true
}