Get your API key
API reference

Sequences

The Sequences endpoints.

Auth: API key · Base URL: https://api.mailkite.dev

Endpoints

Each row expands to its request and response schema. The address bar follows along, so any endpoint can be linked to directly.

GET /v1/sequences List your sequences, newest first, each with live enrollment counts. Archived sequences are omitted.

List your sequences, newest first, each with live enrollment counts. Archived sequences are omitted.

Response body

list-sequences-response.json
FieldTypeNotes
sequences req sequence[]Your sequences, newest first, each with live enrollment counts. Archived ones are omitted.
sequences[].id req stringSequence id (seq_…).
sequences[].name req stringThe handle you reference in send({ sequence }). Unique per account; 1-64 chars of letters, digits, dot, dash, or underscore.
sequences[].status req "draft" · "active" · "paused" · "archived"Only `active` enrolls. `paused` keeps in-flight contacts parked so unpausing resumes rather than restarts; `archived` retires them.
sequences[].fromstring · nullDefault sender for every send step, on a verified domain. Required once the sequence has an event trigger — an event-triggered enrollment has no message to inherit a sender from. A send that names the sequence always supplies one.
sequences[].inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
sequences[].triggerssequence-trigger[]The doors currently attached.
sequences[].triggers[].id req string
sequences[].triggers[].sequenceId req string
sequences[].triggers[].event req stringThe event name to match, as posted to POST /v1/events.
sequences[].triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
sequences[].triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
sequences[].triggers[].createdAt req number
sequences[].triggers[].updatedAtnumber
sequences[].steps req sequence-step[]The program, in order.
sequences[].steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
sequences[].steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
sequences[].steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
sequences[].steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
sequences[].steps[].htmlstringsend only: HTML body.
sequences[].steps[].textstringsend only: plain-text body.
sequences[].steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
sequences[].steps[].replyTostringsend only: Reply-To for this step.
sequences[].steps[].ifrulecondition only: the rule to evaluate.
sequences[].steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
sequences[].steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
sequences[].steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
sequences[].steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
sequences[].steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
sequences[].steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
sequences[].steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
sequences[].steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
sequences[].steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
sequences[].version req numberBumped whenever trigger or steps change. Pinned per enrollment.
sequences[].reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
sequences[].exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
sequences[].exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequences[].exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequences[].enrollmentsobjectLive counts keyed by enrollment status, e.g. { "active": 12, "completed": 340 }.
sequences[].createdAt req numberms epoch.
sequences[].updatedAt req numberms epoch.
POST /v1/sequences Create a sequence: a declared input shape, the steps a contact walks over time, and zero or more triggers. A sequence is a function — `input` is its signature, and every door must satisfy it, so a step's {{input.field}} means the same thing however it was started. Created as a draft unless you pass status "active". The whole definition is validated up front and every problem is reported at once, so you fix a program in one pass rather than one 400 at a time.

Create a sequence: a declared input shape, the steps a contact walks over time, and zero or more triggers. A sequence is a function — `input` is its signature, and every door must satisfy it, so a step's {{input.field}} means the same thing however it was started. Created as a draft unless you pass status "active". The whole definition is validated up front and every problem is reported at once, so you fix a program in one pass rather than one 400 at a time.

Request body

create-sequence-request.json
FieldTypeNotes
name req stringUnique handle for this account; 1-64 chars of letters, digits, dot, dash, or underscore, starting alphanumeric.
status"draft" · "active" · "paused" · "archived"Defaults to `draft`. A sequence enrolls nobody until it is `active`.
fromstringDefault sender for every send step, on a verified domain. Required unless every send step carries its own `from`, or trigger.type is "send" (which inherits the triggering message's sender).
inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
triggersstring · create-trigger-request[]Doors into this sequence. A bare string is shorthand for { "event": "<name>" }. Replacing the set never bumps the sequence's version and never touches an enrollment in flight. A sequence with no triggers is enrolled only by naming it on a send or calling enroll — which is what "manual" used to mean.
steps req sequence-step[]At least one step, and at least one of them a `send` — a sequence that never sends is a no-op that still costs a row per contact.
steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].htmlstringsend only: HTML body.
steps[].textstringsend only: plain-text body.
steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].replyTostringsend only: Reply-To for this step.
steps[].ifrulecondition only: the rule to evaluate.
steps[].if.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
steps[].if.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
steps[].if.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
steps[].if.andrule[]All of these must hold.
steps[].if.orrule[]At least one of these must hold.
steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].then[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].then[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].then[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].then[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].then[].htmlstringsend only: HTML body.
steps[].then[].textstringsend only: plain-text body.
steps[].then[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].then[].replyTostringsend only: Reply-To for this step.
steps[].then[].ifrulecondition only: the rule to evaluate.
steps[].then[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].then[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].then[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].then[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].then[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].then[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].then[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].then[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].then[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].else[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].else[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].else[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].else[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].else[].htmlstringsend only: HTML body.
steps[].else[].textstringsend only: plain-text body.
steps[].else[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].else[].replyTostringsend only: Reply-To for this step.
steps[].else[].ifrulecondition only: the rule to evaluate.
steps[].else[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].else[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].else[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].else[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].else[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].else[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].else[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].else[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].else[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].filter.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
steps[].filter.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
steps[].filter.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
steps[].filter.andrule[]All of these must hold.
steps[].filter.orrule[]At least one of these must hold.
steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
exitOn.goal.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
exitOn.goal.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
exitOn.goal.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
exitOn.goal.andrule[]All of these must hold.
exitOn.goal.orrule[]At least one of these must hold.
exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
exitOn.cancel.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
exitOn.cancel.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
exitOn.cancel.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
exitOn.cancel.andrule[]All of these must hold.
exitOn.cancel.orrule[]At least one of these must hold.

Response body

sequence-response.json
FieldTypeNotes
sequence req sequenceAn automated series of emails: zero or more triggers, a declared input shape, then steps a contact walks over time. Only an `active` sequence enrolls anyone. Editing the STEPS bumps `version` and contacts already in flight keep walking the version they started on, so an edit can never make someone skip or repeat a step; nothing else about a sequence — a rename, a pause, its signature, its triggers — re-pins anybody.
sequence.id req stringSequence id (seq_…).
sequence.name req stringThe handle you reference in send({ sequence }). Unique per account; 1-64 chars of letters, digits, dot, dash, or underscore.
sequence.status req "draft" · "active" · "paused" · "archived"Only `active` enrolls. `paused` keeps in-flight contacts parked so unpausing resumes rather than restarts; `archived` retires them.
sequence.fromstring · nullDefault sender for every send step, on a verified domain. Required once the sequence has an event trigger — an event-triggered enrollment has no message to inherit a sender from. A send that names the sequence always supplies one.
sequence.inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
sequence.triggerssequence-trigger[]The doors currently attached.
sequence.triggers[].id req string
sequence.triggers[].sequenceId req string
sequence.triggers[].event req stringThe event name to match, as posted to POST /v1/events.
sequence.triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
sequence.triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
sequence.triggers[].createdAt req number
sequence.triggers[].updatedAtnumber
sequence.steps req sequence-step[]The program, in order.
sequence.steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
sequence.steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
sequence.steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
sequence.steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
sequence.steps[].htmlstringsend only: HTML body.
sequence.steps[].textstringsend only: plain-text body.
sequence.steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
sequence.steps[].replyTostringsend only: Reply-To for this step.
sequence.steps[].ifrulecondition only: the rule to evaluate.
sequence.steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
sequence.steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
sequence.steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
sequence.steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
sequence.steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
sequence.steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
sequence.steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
sequence.steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
sequence.steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
sequence.version req numberBumped whenever trigger or steps change. Pinned per enrollment.
sequence.reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
sequence.exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
sequence.exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.enrollmentsobjectLive counts keyed by enrollment status, e.g. { "active": 12, "completed": 340 }.
sequence.createdAt req numberms epoch.
sequence.updatedAt req numberms epoch.
enrollmentsobjectLive counts keyed by enrollment status.
triggerssequence-trigger[]
triggers[].id req string
triggers[].sequenceId req string
triggers[].event req stringThe event name to match, as posted to POST /v1/events.
triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
triggers[].createdAt req number
triggers[].updatedAtnumber
GET /v1/sequences/:id Get one sequence with its definition and live enrollment counts.

Get one sequence with its definition and live enrollment counts.

Response body

sequence-response.json
FieldTypeNotes
sequence req sequenceAn automated series of emails: zero or more triggers, a declared input shape, then steps a contact walks over time. Only an `active` sequence enrolls anyone. Editing the STEPS bumps `version` and contacts already in flight keep walking the version they started on, so an edit can never make someone skip or repeat a step; nothing else about a sequence — a rename, a pause, its signature, its triggers — re-pins anybody.
sequence.id req stringSequence id (seq_…).
sequence.name req stringThe handle you reference in send({ sequence }). Unique per account; 1-64 chars of letters, digits, dot, dash, or underscore.
sequence.status req "draft" · "active" · "paused" · "archived"Only `active` enrolls. `paused` keeps in-flight contacts parked so unpausing resumes rather than restarts; `archived` retires them.
sequence.fromstring · nullDefault sender for every send step, on a verified domain. Required once the sequence has an event trigger — an event-triggered enrollment has no message to inherit a sender from. A send that names the sequence always supplies one.
sequence.inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
sequence.triggerssequence-trigger[]The doors currently attached.
sequence.triggers[].id req string
sequence.triggers[].sequenceId req string
sequence.triggers[].event req stringThe event name to match, as posted to POST /v1/events.
sequence.triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
sequence.triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
sequence.triggers[].createdAt req number
sequence.triggers[].updatedAtnumber
sequence.steps req sequence-step[]The program, in order.
sequence.steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
sequence.steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
sequence.steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
sequence.steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
sequence.steps[].htmlstringsend only: HTML body.
sequence.steps[].textstringsend only: plain-text body.
sequence.steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
sequence.steps[].replyTostringsend only: Reply-To for this step.
sequence.steps[].ifrulecondition only: the rule to evaluate.
sequence.steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
sequence.steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
sequence.steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
sequence.steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
sequence.steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
sequence.steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
sequence.steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
sequence.steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
sequence.steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
sequence.version req numberBumped whenever trigger or steps change. Pinned per enrollment.
sequence.reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
sequence.exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
sequence.exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.enrollmentsobjectLive counts keyed by enrollment status, e.g. { "active": 12, "completed": 340 }.
sequence.createdAt req numberms epoch.
sequence.updatedAt req numberms epoch.
enrollmentsobjectLive counts keyed by enrollment status.
triggerssequence-trigger[]
triggers[].id req string
triggers[].sequenceId req string
triggers[].event req stringThe event name to match, as posted to POST /v1/events.
triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
triggers[].createdAt req number
triggers[].updatedAtnumber
PATCH /v1/sequences/:id Edit a sequence. Changing the STEPS bumps its version and contacts already in flight keep walking the version they started on, so an edit can never make someone skip or repeat a step. Nothing else re-pins anybody — a rename, a pause, the input signature, and the trigger set are all editable while people are mid-flight. Archiving retires everyone in flight; pausing deliberately does not, so unpausing resumes rather than restarts.

Edit a sequence. Changing the STEPS bumps its version and contacts already in flight keep walking the version they started on, so an edit can never make someone skip or repeat a step. Nothing else re-pins anybody — a rename, a pause, the input signature, and the trigger set are all editable while people are mid-flight. Archiving retires everyone in flight; pausing deliberately does not, so unpausing resumes rather than restarts.

Request body

update-sequence-request.json

Every field is optional; omitted fields are left alone. Changing `trigger` or `steps` bumps the sequence's version, and contacts already in flight keep walking the version they started on.

FieldTypeNotes
namestring
status"draft" · "active" · "paused" · "archived"
fromstring
inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
triggersstring · create-trigger-request[]Doors into this sequence. A bare string is shorthand for { "event": "<name>" }. Replacing the set never bumps the sequence's version and never touches an enrollment in flight. A sequence with no triggers is enrolled only by naming it on a send or calling enroll — which is what "manual" used to mean.
stepssequence-step[]
steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].htmlstringsend only: HTML body.
steps[].textstringsend only: plain-text body.
steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].replyTostringsend only: Reply-To for this step.
steps[].ifrulecondition only: the rule to evaluate.
steps[].if.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
steps[].if.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
steps[].if.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
steps[].if.andrule[]All of these must hold.
steps[].if.orrule[]At least one of these must hold.
steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].then[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].then[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].then[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].then[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].then[].htmlstringsend only: HTML body.
steps[].then[].textstringsend only: plain-text body.
steps[].then[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].then[].replyTostringsend only: Reply-To for this step.
steps[].then[].ifrulecondition only: the rule to evaluate.
steps[].then[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].then[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].then[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].then[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].then[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].then[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].then[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].then[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].then[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].else[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
steps[].else[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
steps[].else[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
steps[].else[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
steps[].else[].htmlstringsend only: HTML body.
steps[].else[].textstringsend only: plain-text body.
steps[].else[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
steps[].else[].replyTostringsend only: Reply-To for this step.
steps[].else[].ifrulecondition only: the rule to evaluate.
steps[].else[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
steps[].else[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
steps[].else[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].else[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].else[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].else[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].else[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].else[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].else[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
steps[].filter.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
steps[].filter.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
steps[].filter.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
steps[].filter.andrule[]All of these must hold.
steps[].filter.orrule[]At least one of these must hold.
steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
exitOn.goal.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
exitOn.goal.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
exitOn.goal.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
exitOn.goal.andrule[]All of these must hold.
exitOn.goal.orrule[]At least one of these must hold.
exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
exitOn.cancel.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
exitOn.cancel.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
exitOn.cancel.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
exitOn.cancel.andrule[]All of these must hold.
exitOn.cancel.orrule[]At least one of these must hold.

Response body

sequence-response.json
FieldTypeNotes
sequence req sequenceAn automated series of emails: zero or more triggers, a declared input shape, then steps a contact walks over time. Only an `active` sequence enrolls anyone. Editing the STEPS bumps `version` and contacts already in flight keep walking the version they started on, so an edit can never make someone skip or repeat a step; nothing else about a sequence — a rename, a pause, its signature, its triggers — re-pins anybody.
sequence.id req stringSequence id (seq_…).
sequence.name req stringThe handle you reference in send({ sequence }). Unique per account; 1-64 chars of letters, digits, dot, dash, or underscore.
sequence.status req "draft" · "active" · "paused" · "archived"Only `active` enrolls. `paused` keeps in-flight contacts parked so unpausing resumes rather than restarts; `archived` retires them.
sequence.fromstring · nullDefault sender for every send step, on a verified domain. Required once the sequence has an event trigger — an event-triggered enrollment has no message to inherit a sender from. A send that names the sequence always supplies one.
sequence.inputobject · nullThe sequence's input signature: the shape every door must supply. A sequence is a function and this is how you declare its parameters — steps then read them as {{input.field}}, and a condition can branch on them. Omit it entirely for "anything goes"; you should not need a schema to write a two-step drip.
sequence.triggerssequence-trigger[]The doors currently attached.
sequence.triggers[].id req string
sequence.triggers[].sequenceId req string
sequence.triggers[].event req stringThe event name to match, as posted to POST /v1/events.
sequence.triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
sequence.triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
sequence.triggers[].createdAt req number
sequence.triggers[].updatedAtnumber
sequence.steps req sequence-step[]The program, in order.
sequence.steps[].type req "delay" · "send" · "exit" · "condition" · "wait_for_event" · "branch_ai"Which step this is.
sequence.steps[].forstringdelay only: how long to wait — "3 days", "90 minutes", "2h". Maximum 30 days per step.
sequence.steps[].templateIdstringsend only: a saved template (tpl_…) or base template (base_…) supplying subject/html/text. Explicit fields below override it.
sequence.steps[].subjectstringsend only: subject line. Required unless a templateId supplies one. Merge tags are resolved at fire time.
sequence.steps[].htmlstringsend only: HTML body.
sequence.steps[].textstringsend only: plain-text body.
sequence.steps[].fromstringsend only: override the sequence's sender for this step. Must be on a verified domain.
sequence.steps[].replyTostringsend only: Reply-To for this step.
sequence.steps[].ifrulecondition only: the rule to evaluate.
sequence.steps[].thensequence-step[]condition: steps to run when the rule holds. wait_for_event: steps to run when the event arrives. Give a wait neither arm and both outcomes simply continue to the next step.
sequence.steps[].elsesequence-step[]condition: steps to run when the rule does not hold — a condition needs at least one of `then` or `else`. wait_for_event: steps to run when the wait times out instead.
sequence.steps[].eventstringwait_for_event only: the event name to wait for. Sent to POST /v1/events.
sequence.steps[].timeoutstringwait_for_event only: how long to wait before giving up. Omitted = 30 days, the maximum. Timing out takes the `else` arm, or — with no arms — simply continues to the next step. That direction is deliberate: the follow-up is the default and the event is the escape, so a never-sent event cannot silently cancel a campaign.
sequence.steps[].filterrulewait_for_event only: wake only on an occurrence whose payload also satisfies this.
sequence.steps[].promptstringbranch_ai only: what to decide. The contact and the trigger payload are supplied as context automatically — the model sees exactly what a condition or a merge tag would. Max 2000 characters.
sequence.steps[].choicesstring[]branch_ai only: the allowed answers, two to six of them. The model must answer with exactly one; anything else is treated as untrusted and `default` is taken.
sequence.steps[].defaultstringbranch_ai only, REQUIRED: which choice to take when the model is unavailable, slow, refuses, or answers something not on the list. This is what makes an AI step deterministic when the AI is not there.
sequence.steps[].branchesobjectbranch_ai only: steps per choice, keyed by choice name. A choice with no branch simply continues to the next step.
sequence.version req numberBumped whenever trigger or steps change. Pinned per enrollment.
sequence.reentrystring`once` (default) refuses to enroll a contact already in flight. `always` restarts them: the in-flight enrollment is canceled and a new one begins. `cooldown:<duration>` (e.g. "cooldown:30 days") re-enrolls only when that long has passed since their last enrollment.
sequence.exitOnobject · nullSequence-level exits, evaluated before EVERY step. `goal` completes the enrollment — they did the thing, stop chasing, which is the difference between a dunning sequence and harassment. `cancel` abandons it.
sequence.exitOn.goalruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.exitOn.cancelruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
sequence.enrollmentsobjectLive counts keyed by enrollment status, e.g. { "active": 12, "completed": 340 }.
sequence.createdAt req numberms epoch.
sequence.updatedAt req numberms epoch.
enrollmentsobjectLive counts keyed by enrollment status.
triggerssequence-trigger[]
triggers[].id req string
triggers[].sequenceId req string
triggers[].event req stringThe event name to match, as posted to POST /v1/events.
triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
triggers[].createdAt req number
triggers[].updatedAtnumber
DELETE /v1/sequences/:id Delete a sequence and retire every contact still walking it. The response reports how many were canceled.

Delete a sequence and retire every contact still walking it. The response reports how many were canceled.

Response body

delete-sequence-response.json
FieldTypeNotes
id req string
deleted req boolean
enrollmentsCancelednumberHow many in-flight contacts were retired by the delete.
GET /v1/sequences/:id/triggers List the triggers attached to a sequence — the doors into it.

List the triggers attached to a sequence — the doors into it.

Response body

list-triggers-response.json
FieldTypeNotes
triggers req sequence-trigger[]
triggers[].id req string
triggers[].sequenceId req string
triggers[].event req stringThe event name to match, as posted to POST /v1/events.
triggers[].filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
triggers[].enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
triggers[].createdAt req number
triggers[].updatedAtnumber
POST /v1/sequences/:id/triggers Attach a trigger: when this event arrives, enroll the contact it is about. Attaching never bumps the sequence's version and never touches anyone already in flight — a trigger is a fact about the outside world, a sequence is a program, and they change on different rhythms. The sequence needs a `from` address first, since an event-triggered enrollment has no message to inherit a sender from.

Attach a trigger: when this event arrives, enroll the contact it is about. Attaching never bumps the sequence's version and never touches anyone already in flight — a trigger is a fact about the outside world, a sequence is a program, and they change on different rhythms. The sequence needs a `from` address first, since an event-triggered enrollment has no message to inherit a sender from.

Request body

create-trigger-request.json

Attach a door to a sequence. The sequence must have a `from` address: an event-triggered enrollment has no message to inherit a sender from. One trigger per (sequence, event) — a duplicate would enroll twice, and `reentry: "once"` would absorb the second silently.

FieldTypeNotes
event req string
filterruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
filter.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.andrule[]All of these must hold.
filter.and[].fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.and[].op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.and[].valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.and[].andrule[]All of these must hold.
filter.and[].orrule[]At least one of these must hold.
filter.orrule[]At least one of these must hold.
filter.or[].fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.or[].op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.or[].valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.or[].andrule[]All of these must hold.
filter.or[].orrule[]At least one of these must hold.
enabledbooleanDefaults to true.

Response body

trigger-response.json
FieldTypeNotes
trigger req sequence-triggerA door into a sequence: when this event arrives, enroll the contact it is about. Its own resource, because a trigger is a fact about the outside world while a sequence is a program — they change on completely different rhythms. Attaching, detaching, enabling, or disabling one never bumps the sequence's version and never touches an enrollment already in flight.
trigger.id req string
trigger.sequenceId req string
trigger.event req stringThe event name to match, as posted to POST /v1/events.
trigger.filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
trigger.enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
trigger.createdAt req number
trigger.updatedAtnumber
PATCH /v1/triggers/:id Edit a trigger, or toggle `enabled` to switch the door off without deleting it. Either way, everyone already walking the sequence carries on.

Edit a trigger, or toggle `enabled` to switch the door off without deleting it. Either way, everyone already walking the sequence carries on.

Request body

update-trigger-request.json

Every field optional. Toggling `enabled` is the ad-hoc lever: it stops future enrollments immediately and leaves everyone in flight alone.

FieldTypeNotes
eventstring
filterruleA predicate over the enrolled contact and the trigger payload. Either a comparison — `field`, `op`, and (for binary operators) `value` — or a group: `{ "and": [...] }` / `{ "or": [...] }`. Fields live in two namespaces, `event.*` (the trigger payload) and `contact.*` (stored contact properties), plus bare `email` and `name`. This is the same operator set and shape used by Resend, Loops, and Customer.io, so an automation written against one of those ports across unchanged. A missing field is falsy for every operator except `exists` (false) and `is_empty` (true).
filter.fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.andrule[]All of these must hold.
filter.and[].fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.and[].op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.and[].valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.and[].andrule[]All of these must hold.
filter.and[].orrule[]At least one of these must hold.
filter.orrule[]At least one of these must hold.
filter.or[].fieldstringDotted path into the context, e.g. "event.plan" or "contact.company".
filter.or[].op"eq" · "neq" · "gt" · "gte" · "lt" · "lte" · "contains" · "starts_with" · "ends_with" · "exists" · "is_empty"Comparison operator. `exists` and `is_empty` take no `value`.
filter.or[].valuestring · number · boolean · nullWhat to compare against. String and number are compared loosely, so a payload carrying "3" still matches a rule written against 3.
filter.or[].andrule[]All of these must hold.
filter.or[].orrule[]At least one of these must hold.
enabledboolean

Response body

trigger-response.json
FieldTypeNotes
trigger req sequence-triggerA door into a sequence: when this event arrives, enroll the contact it is about. Its own resource, because a trigger is a fact about the outside world while a sequence is a program — they change on completely different rhythms. Attaching, detaching, enabling, or disabling one never bumps the sequence's version and never touches an enrollment already in flight.
trigger.id req string
trigger.sequenceId req string
trigger.event req stringThe event name to match, as posted to POST /v1/events.
trigger.filterobject · nullOnly open the door when the event's payload also satisfies this rule — "payment.failed, but only over $50". Null means every occurrence opens it. Shape: see the `rule` schema.
trigger.enabled req booleanSwitch the door off without deleting or editing anything. A disabled trigger enrolls nobody; everyone already in flight carries on.
trigger.createdAt req number
trigger.updatedAtnumber
DELETE /v1/triggers/:id Detach a trigger. Stops future enrollments through that door and nothing else.

Detach a trigger. Stops future enrollments through that door and nothing else.

Response body

delete-trigger-response.json
FieldTypeNotes
id req string
deleted req boolean
POST /v1/sequences/:id/enroll Put one contact into an active sequence by hand, with the input its signature declares. Refused with 409 when the address is suppressed, already in flight on a `reentry: "once"` sequence, or missing a required input — and the response says which. Pass `cancelKey` to be able to cancel later using your own identifier.

Put one contact into an active sequence by hand, with the input its signature declares. Refused with 409 when the address is suppressed, already in flight on a `reentry: "once"` sequence, or missing a required input — and the response says which. Pass `cancelKey` to be able to cancel later using your own identifier.

Request body

enroll-request.json

Put one contact into a sequence by hand. The sequence must be `active`. A suppressed address, or one already in flight on a `reentry: "once"` sequence, is refused with 409 rather than silently ignored.

FieldTypeNotes
emailstringThe address to enroll. Give this or `contactId`.
contactIdstringA contact you own (ctc_…), as an alternative to `email`.
fromstringSender for this enrollment, when the sequence has no default (a send-triggered sequence normally inherits one from the triggering message, which a manual enroll does not have).
inputobjectThe sequence's input, checked against its declared signature. A missing required field refuses the enrollment and says which — you find out here rather than days later as a blank merge tag.
dataobjectThe original name for `input`. Still accepted; `input` wins when both are given.
cancelKeystringYour own key for this enrollment. Cancel later with POST /v1/enrollments/cancel and this key — no need to store our id.

Response body

enrollment-response.json
FieldTypeNotes
enrollment req enrollmentOne contact walking one sequence — where they are, and what happens next. `active` means a step is due at `nextAt`; `waiting` means parked on an event; `completed`, `canceled`, and `failed` are terminal.
enrollment.id req stringEnrollment id (enr_…).
enrollment.sequenceId req string
enrollment.sequenceVersionnumberThe sequence version this contact is walking, pinned at enrollment.
enrollment.email req stringThe enrolled address, normalized to lowercase.
enrollment.contactIdstring · nullThe contact this resolved to, when the address is one you hold.
enrollment.fromstringThe sender frozen at enrollment — send steps use it unless they override with their own `from`.
enrollment.status req "active" · "waiting" · "completed" · "canceled" · "failed"
enrollment.stepIndex req numberThe next step to run, indexed into the compiled program.
enrollment.nextAtnumber · nullWhen the next step is due, in ms epoch. Null while parked on an event or terminal.
enrollment.waitEventstring · nullThe event name this enrollment is parked on.
enrollment.waitUntilnumber · nullWhen the parked wait times out, in ms epoch.
enrollment.inputobject · nullThe validated input this contact was enrolled with. Steps read it as {{input.*}} — and {{event.*}}, the original alias.
enrollment.triggerobject · nullHow the enrollment started: { type: 'event'|'send'|'manual', event?, messageId?, from?, to?, subject?, templateId? }. Steps read it as {{trigger.*}}. `input` is what you passed; this is how it started.
enrollment.cancelKeystring · nullYour own key for this enrollment — cancel by it without needing our id.
enrollment.errorstring · nullWhy a failed enrollment stopped.
enrollment.createdAt req number
enrollment.updatedAtnumber
GET /v1/sequences/:id/enrollments List who is in a sequence and where each of them is. Filter with `status`.

List who is in a sequence and where each of them is. Filter with `status`.

Response body

list-enrollments-response.json
FieldTypeNotes
enrollments req enrollment[]Enrollments on this sequence, newest first.
enrollments[].id req stringEnrollment id (enr_…).
enrollments[].sequenceId req string
enrollments[].sequenceVersionnumberThe sequence version this contact is walking, pinned at enrollment.
enrollments[].email req stringThe enrolled address, normalized to lowercase.
enrollments[].contactIdstring · nullThe contact this resolved to, when the address is one you hold.
enrollments[].fromstringThe sender frozen at enrollment — send steps use it unless they override with their own `from`.
enrollments[].status req "active" · "waiting" · "completed" · "canceled" · "failed"
enrollments[].stepIndex req numberThe next step to run, indexed into the compiled program.
enrollments[].nextAtnumber · nullWhen the next step is due, in ms epoch. Null while parked on an event or terminal.
enrollments[].waitEventstring · nullThe event name this enrollment is parked on.
enrollments[].waitUntilnumber · nullWhen the parked wait times out, in ms epoch.
enrollments[].inputobject · nullThe validated input this contact was enrolled with. Steps read it as {{input.*}} — and {{event.*}}, the original alias.
enrollments[].triggerobject · nullHow the enrollment started: { type: 'event'|'send'|'manual', event?, messageId?, from?, to?, subject?, templateId? }. Steps read it as {{trigger.*}}. `input` is what you passed; this is how it started.
enrollments[].cancelKeystring · nullYour own key for this enrollment — cancel by it without needing our id.
enrollments[].errorstring · nullWhy a failed enrollment stopped.
enrollments[].createdAt req number
enrollments[].updatedAtnumber
GET /v1/enrollments/:id Get one enrollment — which sequence, which step, and what happens next.

Get one enrollment — which sequence, which step, and what happens next.

Response body

enrollment-response.json
FieldTypeNotes
enrollment req enrollmentOne contact walking one sequence — where they are, and what happens next. `active` means a step is due at `nextAt`; `waiting` means parked on an event; `completed`, `canceled`, and `failed` are terminal.
enrollment.id req stringEnrollment id (enr_…).
enrollment.sequenceId req string
enrollment.sequenceVersionnumberThe sequence version this contact is walking, pinned at enrollment.
enrollment.email req stringThe enrolled address, normalized to lowercase.
enrollment.contactIdstring · nullThe contact this resolved to, when the address is one you hold.
enrollment.fromstringThe sender frozen at enrollment — send steps use it unless they override with their own `from`.
enrollment.status req "active" · "waiting" · "completed" · "canceled" · "failed"
enrollment.stepIndex req numberThe next step to run, indexed into the compiled program.
enrollment.nextAtnumber · nullWhen the next step is due, in ms epoch. Null while parked on an event or terminal.
enrollment.waitEventstring · nullThe event name this enrollment is parked on.
enrollment.waitUntilnumber · nullWhen the parked wait times out, in ms epoch.
enrollment.inputobject · nullThe validated input this contact was enrolled with. Steps read it as {{input.*}} — and {{event.*}}, the original alias.
enrollment.triggerobject · nullHow the enrollment started: { type: 'event'|'send'|'manual', event?, messageId?, from?, to?, subject?, templateId? }. Steps read it as {{trigger.*}}. `input` is what you passed; this is how it started.
enrollment.cancelKeystring · nullYour own key for this enrollment — cancel by it without needing our id.
enrollment.errorstring · nullWhy a failed enrollment stopped.
enrollment.createdAt req number
enrollment.updatedAtnumber
GET /v1/enrollments/:id/runs Every step this enrollment has executed, with the outcome and the reason for it. This is the "why didn't step 3 fire" view.

Every step this enrollment has executed, with the outcome and the reason for it. This is the "why didn't step 3 fire" view.

Response body

list-runs-response.json
FieldTypeNotes
enrollment req enrollmentOne contact walking one sequence — where they are, and what happens next. `active` means a step is due at `nextAt`; `waiting` means parked on an event; `completed`, `canceled`, and `failed` are terminal.
enrollment.id req stringEnrollment id (enr_…).
enrollment.sequenceId req string
enrollment.sequenceVersionnumberThe sequence version this contact is walking, pinned at enrollment.
enrollment.email req stringThe enrolled address, normalized to lowercase.
enrollment.contactIdstring · nullThe contact this resolved to, when the address is one you hold.
enrollment.fromstringThe sender frozen at enrollment — send steps use it unless they override with their own `from`.
enrollment.status req "active" · "waiting" · "completed" · "canceled" · "failed"
enrollment.stepIndex req numberThe next step to run, indexed into the compiled program.
enrollment.nextAtnumber · nullWhen the next step is due, in ms epoch. Null while parked on an event or terminal.
enrollment.waitEventstring · nullThe event name this enrollment is parked on.
enrollment.waitUntilnumber · nullWhen the parked wait times out, in ms epoch.
enrollment.inputobject · nullThe validated input this contact was enrolled with. Steps read it as {{input.*}} — and {{event.*}}, the original alias.
enrollment.triggerobject · nullHow the enrollment started: { type: 'event'|'send'|'manual', event?, messageId?, from?, to?, subject?, templateId? }. Steps read it as {{trigger.*}}. `input` is what you passed; this is how it started.
enrollment.cancelKeystring · nullYour own key for this enrollment — cancel by it without needing our id.
enrollment.errorstring · nullWhy a failed enrollment stopped.
enrollment.createdAt req number
enrollment.updatedAtnumber
runs req step-run[]Executed steps in order.
runs[].id req string
runs[].stepIndex req numberWhich step in the compiled program this was.
runs[].stepType req stringdelay | send | exit | condition | wait_for_event | branch_ai.
runs[].outcome req "sent" · "skipped" · "branched" · "waited" · "failed" · "scheduled" · "completed"
runs[].detailstring · nullHuman-readable reason for the outcome.
runs[].messageIdstring · nullThe message a `send` step produced (msg_…).
runs[].ranAt req numberms epoch.
DELETE /v1/enrollments/:id Stop a contact partway through a sequence. Answers 409 when the enrollment has already finished or been canceled.

Stop a contact partway through a sequence. Answers 409 when the enrollment has already finished or been canceled.

Response body

cancel-enrollment-response.json
FieldTypeNotes
id req string
status req string
POST /v1/enrollments/cancel Stop every in-flight enrollment carrying a `cancelKey` you set when enrolling — so you can cancel with your own invoice or order id instead of storing ours. Always answers 200 with a count, so it is safe to fire blindly from a webhook.

Stop every in-flight enrollment carrying a `cancelKey` you set when enrolling — so you can cancel with your own invoice or order id instead of storing ours. Always answers 200 with a count, so it is safe to fire blindly from a webhook.

Request body

cancel-by-key-request.json

Stop every in-flight enrollment carrying this key. The point is that you never have to store our enrollment id — you already have the invoice number, order id, or subscription id the sequence is about.

FieldTypeNotes
cancelKey req stringThe key you passed when enrolling.

Response body

cancel-by-key-response.json

Always 200 with a count, never 404 — cancelling something already finished is a no-op you can fire blindly from a webhook without branching on our state.

FieldTypeNotes
cancelKey req string
canceled req numberHow many in-flight enrollments were stopped. Zero is a normal answer.