Quickstart
From docker compose up to a first enrollment, a sent email and a reply, in six calls.
You need Docker and curl. The compose file starts Postgres and one OpenSequence process with mock providers on: nothing reaches Google, Allo or Attio, sends land in the contact's timeline, and a hook lets you simulate a reply. The operator token is dev-admin-token.
Run it
git clone https://github.com/lguerberg/open-sequence && cd open-sequence
docker compose up
# in another shell
export SEQ=http://localhost:8080
curl $SEQ/readyz # {"ok":true}Create a project and its API key
The operator token creates the tenant and returns its first admin-scoped key. The key is shown once.
curl -X POST $SEQ/v1/admin/projects \
-H "Authorization: Bearer dev-admin-token" -H "Content-Type: application/json" \
-d '{"slug":"acme","name":"Acme CRM"}'
# {"success":true,"data":{"project":{"id":"prj_…","slug":"acme",…},"api_key":"sk_live_…"},"meta":{"request_id":"req_…"}}
export KEY=sk_live_…A sender, and connect their Google account
A sender is the acting user an enrollment runs as. In production the connect call returns a consent URL the sender opens once (Gmail and Calendar). With mock providers the mailbox is active at once.
curl -X PUT $SEQ/v1/senders/u_42 -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"name":"Max Rivera","email":"max@acme.com","timezone":"America/New_York"}'
curl -X POST $SEQ/v1/senders/u_42/connections/google -H "Authorization: Bearer $KEY" -H "Content-Type: application/json"
# mock: {"success":true,"data":{"status":"active","mock":true,"channel":{"channel":"email","address":"max@acme.com",…}}}
# real: {"success":true,"data":{"auth_url":"https://accounts.google.com/o/oauth2/v2/auth?…","connection_id":"con_…"}}A contact
PUT by your own id; attrs is free JSON that conditions and templates can read.
curl -X PUT $SEQ/v1/contacts/p_7 -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"first_name":"Dana","email":"dana@example.com","timezone":"Europe/London","attrs":{"company":{"country":"UK"}}}'A sequence: create, save the definition, publish
Creating a sequence makes a draft version v1. Save a definition into it, then publish: validation errors block a publish, warnings do not.
{
"schema_version": 1,
"entry": "start",
"settings": { "reenroll": "never" },
"exit_on": ["reply", "meeting_booked"],
"nodes": {
"start": { "type": "start", "next": { "then": "email_intro" } },
"email_intro": {
"type": "outreach", "channel": "email", "action": "send",
"params": {
"subject": "Quick question, {{ contact.first_name | \"there\" }}",
"body": "Hi {{ contact.first_name | \"there\" }}, from {{ sender.name }}.\n\nUnsubscribe: {{ unsubscribe_url }}"
},
"next": { "sent": "wait_reply", "skipped": "wait_reply", "failed": "end" }
},
"wait_reply": { "type": "wait_for", "params": { "events": ["reply", "meeting_booked"], "timeout": "3d" }, "next": { "reply": "end", "meeting_booked": "end", "timeout": "call" } },
"call": { "type": "task", "params": { "kind": "call", "title": "Call {{ contact.first_name | \"them\" }}", "due": "1d", "dialer": true }, "next": { "completed": "end", "no_answer": "end" } },
"end": { "type": "end" }
}
}curl -X POST $SEQ/v1/sequences -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"name":"UK intro"}'
# {"success":true,"data":{"sequence":{"id":"seq_…"},"version":{"id":"ver_…","tag":"v1","status":"draft","lockVersion":0}}}
export SEQ_ID=seq_… VER_ID=ver_…
curl -X PUT $SEQ/v1/sequences/$SEQ_ID/versions/$VER_ID/draft -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{\"definition\": $(cat definition.json), \"lock_version\": 0}"
# {"success":true,"data":{"version":{…},"problems":[]}}
curl -X POST $SEQ/v1/sequences/$SEQ_ID/versions/$VER_ID/publish -H "Authorization: Bearer $KEY" -H "Content-Type: application/json"
# {"success":true,"data":{"version":{"status":"published",…},"problems":[]}}Enroll
Enroll the contact as the sender. A contact can be in one active sequence per project; a second enroll of the same contact is a 409 ALREADY_IN_SEQUENCE and nobody is enrolled.
curl -X POST $SEQ/v1/sequences/$SEQ_ID/enrollments -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"contacts":["p_7"],"sender":"u_42"}'
# {"success":true,"data":{"accepted":[{"contact_id":"ctc_…","external_id":"p_7","first_touch_at":"…"}],"skipped":[],"capacity":{"per_day":30,"days_to_first_touches":1},"enrollments":[{"id":"enr_…","status":"active",…}]}}
export ENR_ID=enr_…Read the journey and the sent email
Within a few seconds the planner claims the enrollment and the email executor sends through the mock mailbox. The journey shows the runs, the message and the waiting step.
curl $SEQ/v1/enrollments/$ENR_ID -H "Authorization: Bearer $KEY"
# data.enrollment.currentNodeId == "wait_reply"; data.runs: start done, email_intro done, wait_reply waiting
# data.messages[0]: {"direction":"out","subject":"Quick question, Dana","status":"sent","threadRef":"mock-thread:…"}
curl $SEQ/v1/contacts/p_7/timeline -H "Authorization: Bearer $KEY"Simulate a reply, watch the exit
The mock hook accepts an inbound event. A reply in the thread the service started is matched to the enrollment, attributed to the email step, and the enrollment exits with replied because exit_on lists reply.
curl -X POST $SEQ/hooks/mock/acme -H "Content-Type: application/json" -d '{
"kind": "reply", "channel": "email", "externalId": "evt_1", "occurredAt": "2026-10-13T10:00:00Z",
"from": "dana@example.com", "to": "max@acme.com", "threadRef": "<threadRef from the sent message>"
}'
# {"ok":true,"stored":1,"ignored":0}
curl $SEQ/v1/enrollments/$ENR_ID -H "Authorization: Bearer $KEY"
# data.enrollment.status == "exited", exitReason == "replied"
curl $SEQ/v1/sequences/$SEQ_ID/analytics/summary -H "Authorization: Bearer $KEY"
# {"enrolled":1,"active":0,"exited":1,"touches_delivered":1,"replies":1,"reply_rate_per_100_enrolled":100,"replies_by_channel":{"email":1},…}POST with Content-Type: application/json and no body is accepted everywhere a body is optional (the connect and publish calls above), which is what curl and most SDKs send.Where next
- In production, register your own Google OAuth client before connecting senders: Connect Google.
- Register a webhook endpoint instead of polling the journey: Receiving webhooks.
- Tell the service what it cannot see (meetings booked elsewhere, do-not-contact): Contact events.
- Every node type, with an example: Nodes.