Enrolling
Preview, enroll, dry run, capacity staggering, the 409 ALREADY_IN_SEQUENCE rule and why it is all-or-nothing.
curl -X POST $SEQ/v1/sequences/seq_01J9…/enrollments:preview -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"contacts":["p_7","p_8","p_9"],"sender":"u_42"}'
curl -X POST $SEQ/v1/sequences/seq_01J9…/enrollments -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"contacts":["p_7","p_8",{"external_id":"p_10","email":"lee@example.com"}],"sender":"u_42","enrolled_by":"user_17","enrolled_by_kind":"user"}'The request
| Field | Meaning |
|---|---|
contacts | 1 to 5,000 contact ids, external ids, or inline contact objects ({ external_id, ...fields }, upserted first) |
sender | the sender's id or external id; must be active (409 SENDER_DISABLED) |
version | a version id; default the sequence's default (published) version. A draft is 409 VERSION_NOT_PUBLISHED; no published version is 409 NO_PUBLISHED_VERSION |
enrolled_by, enrolled_by_kind | who did it (user, agent, system) for the audit trail; default the API key id and user |
mode | live (default) or dry_run |
Scope enroll for enrolling, read for the preview. The sequence must be active (409 SEQUENCE_NOT_ACTIVE).
The response
{
"accepted": [{ "contact_id": "ctc_…", "external_id": "p_7", "first_touch_at": "2026-10-13T13:00:00.000Z" }],
"skipped": [{ "contact_id": "ctc_…", "external_id": "p_8", "reason": "suppressed:do_not_contact" }],
"capacity": { "per_day": 30, "days_to_first_touches": 1 },
"enrollments": [{ "id": "enr_…", "contact": "ctc_…", "status": "active", "next_run_at": "2026-10-13T13:00:00.000Z" }]
}The preview returns the same without enrollments and writes nothing. skipped reasons: suppressed:<reason> (an all suppression) and the re-enrollment rule, already_enrolled_before (settings.reenroll: "never") or reenroll_before_<n>d. Skipped contacts do not block the rest.
One active sequence per contact
A contact can be in at most one active sequence per project. When any selected contact is already in one (status active, paused or waiting_limit), the request fails with 409 ALREADY_IN_SEQUENCE, names each conflict, and enrolls nobody. It does not skip them silently and it does not half-enroll the batch, so the person who clicked "enroll 142 people" sees exactly which two to remove.
{
"success": false,
"error": {
"code": "ALREADY_IN_SEQUENCE",
"message": "2 of the 142 contacts are already in a sequence. Nobody was enrolled. Remove them from the selection or exit them from their current sequence first.",
"details": { "conflicts": [
{ "contact_id": "ctc_01J9…", "external_id": "p_4411", "name": "Dana Whitfield", "sequence_id": "seq_01J9…", "sequence_name": "UK facilities managers", "version": "v1", "sender": "Priya Nair", "enrollment_id": "enr_01J9…", "status": "active" }
] }
}
}Enforced in three layers so concurrency cannot break it: the preflight finds conflicts; the insert runs in one transaction; and a partial unique index on (project_id, contact_id) for active enrollments catches a race, which is converted to the same 409 ("enrolled elsewhere at the same moment; try again"). The preview returns the same error, so a UI can show conflicts before the user confirms.
Capacity and staggering
First touches are staggered by the sender's email daily_limit (default 30) and the sequence's send window: contact 31 starts tomorrow, contact 61 the day after, each pushed into the window. capacity.per_day and days_to_first_touches let a dialog say "first touches finish in about 17 days". Within a day the executor's caps and gaps spread sends further.
Dry run
mode: "dry_run" creates real enrollments whose outreach steps are recorded as skipped_dry_run instead of sent (with the rendered values), while waits, conditions and tasks run for real. Use it to rehearse a new sequence with one contact and read the journey; exit the enrollment afterwards.
What happens next
The planner claims the enrollment within five seconds of next_run_at, runs the entry node and queues the first outreach step; enrollment.created is delivered per contact. Follow with the journey or webhooks.