Sequence Enrollments
Enroll contacts into a sequence and track their progress.
Enrolling a contact
A sequence owns a source list (its listId). A contact is enrolled by adding that list to the contact's listIds array — there is no separate "enroll" endpoint:
# 1. Find the sequence's source list id
curl -G "https://api.zero.inc/api/sequences/SEQUENCE_UUID" \
--data-urlencode "fields=id,name,listId" \
-H "Authorization: Bearer YOUR_API_TOKEN"
# 2. Add that list to the contact's listIds — this enrolls them.
# listIds is replaced wholesale, so include the contact's existing lists too.
curl -X PATCH "https://api.zero.inc/api/contacts/CONTACT_UUID" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"listIds": ["EXISTING_LIST_UUID", "SEQUENCE_LIST_UUID"]}'The contact immediately gets an enrollment record (a SequenceContactStatus) with status waitingForPickUp. If the sequence is active, a background worker assigns the first step and begins sending within about a minute. Sequences whose start step uses manual roll-out land in waitingForApproval until approved in the app.
Unenrolling
Remove the sequence's list from the contact's listIds. The enrollment's status flips to cancelled and no further steps run.
Unsubscribing from all messaging
Unenrolling only stops this sequence. To suppress all outbound messaging to a contact — across every sequence, automation, and agent send — set unsubscribedFromAllMessaging: true via PATCH /api/contacts/{contactId} (see the ContactUpdate schema). New enrollments are then blocked and any active run is stopped on its next step, with an outcome of unsubscribed.
Tracking progress
Use GET /api/sequenceContactStatuses for per-contact state, and GET /api/sequences/progressSummary for aggregate per-sequence metrics. POST /api/sequences/reenroll resets contacts to the start; POST /api/sequences/forceNextStep advances them.
Resets the given contacts to the start of the sequence (status waitingForPickUp), clearing their step history, outcome, scheduled emails, enrollment logs, and any AI drafts. Contacts with no existing enrollment row are enrolled fresh. The background worker re-assigns the first step on the next pass (if the sequence is active).
All API requests require a Bearer token in the Authorization header. Create an API key from Workspace Settings → API keys
Contacts re-enrolled
Number of contacts processed.
sequenceId or contactIds missing, or the sequence has no source list
Authentication failed or token is invalid/expired
The requested resource was not found
POST /api/sequences/reenroll HTTP/1.1
Host: api.zero.inc
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 79
{
"sequenceId": "sequence-uuid",
"contactIds": [
"contact-uuid-1",
"contact-uuid-2"
]
}{
"success": true,
"count": 1
}Moves the given contacts from their current step to the next one immediately, cancelling any pending scheduled emails for the current step. Contacts already on the last step are marked completed; contacts with no current step are skipped. When the next step is a delay, the contact parks on the delay and resumes naturally when it elapses.
All API requests require a Bearer token in the Authorization header. Create an API key from Workspace Settings → API keys
Contacts advanced
sequenceId or contactIds missing
Authentication failed or token is invalid/expired
The requested resource was not found
POST /api/sequences/forceNextStep HTTP/1.1
Host: api.zero.inc
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 62
{
"sequenceId": "sequence-uuid",
"contactIds": [
"contact-uuid-1"
]
}{
"success": true,
"advanced": 1,
"completed": 1,
"skipped": 1
}Returns aggregate enrollment metrics per sequence for a workspace (enrolled, active, paused, completed, replied, bounced, etc.). Pass sequenceId to scope the summary to a single sequence.
All API requests require a Bearer token in the Authorization header. Create an API key from Workspace Settings → API keys
Limit the summary to a single sequence.
Successful response
workspaceId missing
Authentication failed or token is invalid/expired
GET /api/sequences/progressSummary?workspaceId=123e4567-e89b-12d3-a456-426614174000 HTTP/1.1
Host: api.zero.inc
Authorization: Bearer YOUR_API_TOKEN
Accept: */*
{
"success": true,
"data": [
{
"sequenceId": "123e4567-e89b-12d3-a456-426614174000",
"enrolled": 1,
"waitingForPickUp": 1,
"waitingForApproval": 1,
"active": 1,
"paused": 1,
"statusCompleted": 1,
"stopped": 1,
"cancelled": 1,
"replied": 1,
"bounced": 1,
"unsubscribed": 1,
"error": 1,
"finished": 1,
"activeOrScheduled": 1,
"errors": 1,
"responseRatePercent": 1
}
]
}Returns enrollment records (SequenceContactStatus) — one per contact per sequence — with each contact's live status, current step, and outcome. Filter with where, e.g. {"workspaceId": "<WORKSPACE_UUID>", "sequenceId": "<SEQUENCE_UUID>"}, optionally adding "status": "active".
All API requests require a Bearer token in the Authorization header. Create an API key from Workspace Settings → API keys
Comma-separated list of fields to return. Defaults to all fields.
Supports relation expansion with dot-notation: a path like company.name embeds the named field of the related record, and quotes.id embeds the id of each related quote. Use .* to embed full related objects, e.g. quotes.*. Related records are returned nested on the response — as an object for to-one relations (e.g. company) or an array for to-many relations (e.g. quotes). Archived related records are excluded.
Example: GET /api/deals/{dealId}?fields=id,name,quotes.id returns the deal with a quotes array of {"id": "<QUOTE_UUID>"} entries.
id,name,domainJSON-encoded filter object. All top-level conditions are combined with AND logic. Use $or for OR logic.
The available operators depend on the data type of the field you are filtering on.
String fields
Fields like name, domain, description, linkedin, source, externalId, location.
| Operator | Description | Example |
|---|---|---|
| (exact) | Exact match | {"name": "Linear"} |
$eq | Explicit exact match | {"name": {"$eq": "Linear"}} |
$not | Not equal | {"source": {"$not": "import"}} |
$in | Matches any value in array | {"domain": {"$in": ["linear.app", "granola.so"]}} |
$notIn | Matches none of the values | {"source": {"$notIn": ["import", "api"]}} |
$contains | Case-insensitive word-boundary substring match | {"name": {"$contains": "YC"}} |
$notContains | Does not contain | {"name": {"$notContains": "Test"}} |
$containsAny | Contains any of the given strings | {"name": {"$containsAny": ["YC", "Techstars"]}} |
$startsWith | Starts with prefix | {"domain": {"$startsWith": "app."}} |
$endsWith | Ends with suffix | {"email": {"$endsWith": "@zero.inc"}} |
$exists | Field is present and truthy | {"linkedin": {"$exists": true}} |
$notExists | Field is absent, null, or empty | {"linkedin": {"$notExists": true}} |
Number fields
Fields like value, confidence.
| Operator | Description | Example |
|---|---|---|
| (exact) | Exact match | {"value": 5000} |
$eq | Explicit exact match | {"value": {"$eq": 5000}} |
$not | Not equal | {"value": {"$not": 0}} |
$gt | Greater than | {"value": {"$gt": 10000}} |
$gte | Greater than or equal | {"value": {"$gte": 5000}} |
$lt | Less than | {"value": {"$lt": 10000}} |
$lte | Less than or equal | {"value": {"$lte": 50000}} |
$in | Matches any value in array | {"confidence": {"$in": [0.25, 0.5, 0.75]}} |
$notIn | Matches none of the values | {"confidence": {"$notIn": [0, 1]}} |
$exists | Field is present and truthy | {"value": {"$exists": true}} |
$notExists | Field is absent, null, or zero | {"value": {"$notExists": true}} |
Multiple operators can be combined on one field:
{"value": {"$gte": 5000, "$lt": 10000}}
Date fields
Fields like closeDate, startDate, endDate, createdAt, updatedAt.
Values can be ISO 8601 strings ("2026-01-01", "2026-01-01T00:00:00Z") or relative time macros.
Relative time macros: +Nd / -Nd (days), +Nw / -Nw (weeks), +Nm / -Nm (months), +Ny / -Ny (years), +Nh / -Nh (hours), +Ns / -Ns (seconds), now().
| Operator | Description | Example |
|---|---|---|
$gte | On or after | {"closeDate": {"$gte": "2026-01-01"}} |
$lte | On or before | {"closeDate": {"$lte": "2026-03-31"}} |
$gt | After | {"createdAt": {"$gt": "2026-01-01T00:00:00Z"}} |
$lt | Before | {"createdAt": {"$lt": "now()"}} |
$date | Exact date match (compares date portion only) | {"closeDate": {"$date": "2026-01-15"}} |
$exists | Field is present and truthy | {"closeDate": {"$exists": true}} |
$notExists | Field is absent or null | {"closeDate": {"$notExists": true}} |
Date range example:
{"closeDate": {"$gte": "2026-01-01", "$lte": "2026-03-31"}}
Relative date example (closing in next 30 days):
{"closeDate": {"$gte": "now()", "$lte": "+30d"}}
Array fields
Fields like listIds, ownerIds, contactIds.
| Operator | Description | Example |
|---|---|---|
$includes | Array contains the given value (use this — bare exact match is not supported) | {"listIds": {"$includes": "<LIST_UUID>"}} |
$notIncludes | Array does not contain the given value | {"ownerIds": {"$notIncludes": "<USER_UUID>"}} |
$overlaps | Array contains at least one of the given values | {"ownerIds": {"$overlaps": ["<UUID_1>", "<UUID_2>"]}} |
$notOverlaps | Array contains none of the given values | {"listIds": {"$notOverlaps": ["<UUID_1>", "<UUID_2>"]}} |
$all | Array contains all of the given values | {"listIds": {"$all": ["<UUID_1>", "<UUID_2>"]}} |
$length | Filter by array length (supports nested operators) | {"contactIds": {"$length": {"$gte": 2}}} |
$exists | Field is present and non-empty | {"ownerIds": {"$exists": true}} |
$notExists | Field is absent or empty | {"ownerIds": {"$notExists": true}} |
UUID / ID fields
Fields like id, workspaceId, companyId, stage.
| Operator | Description | Example |
|---|---|---|
| (exact) | Exact match | {"stage": "<PIPELINE_STAGE_UUID>"} |
$in | Matches any value in array | {"stage": {"$in": ["<UUID_1>", "<UUID_2>"]}} |
$notIn | Matches none of the values | {"stage": {"$notIn": ["<UUID_1>"]}} |
$not | Not equal | {"stage": {"$not": "<UUID>"}} |
Boolean fields
Fields like archived.
| Operator | Description | Example |
|---|---|---|
| (exact) | Exact match | {"archived": false} |
$not | Not equal | {"archived": {"$not": true}} |
Logical operators
These work across all data types.
$or — matches if any sub-condition is true:
{
"workspaceId": "<WORKSPACE_UUID>",
"$or": [
{"ownerIds": {"$overlaps": ["<USER_UUID_1>", "<USER_UUID_2>"]}},
{"closeDate": {"$gte": "2026-01-01"}}
]
}
$and — matches if all sub-conditions are true (useful when you need multiple conditions on the same field):
{
"$and": [
{"name": {"$contains": "Enterprise"}},
{"name": {"$notContains": "Test"}}
]
}
Dot-notation (relation filtering)
Use dot-syntax to filter records based on properties of related objects:
{"company.name": "Linear"}
{"company.domain": {"$in": ["linear.app", "granola.so"]}}
{"company.location.city": "San Francisco"}
{"companyProfile.categories": {"$overlaps": ["Sales", "Marketing"]}}
All operators available for the target field's data type can be used with dot-notation.
Custom property filtering
Custom properties are stored in the custom object on records, keyed by UUID. Use GET /api/columns to discover the available custom property IDs, types, and options for a workspace.
Filter on custom properties using custom.<COLUMN_ID> with operators appropriate for the column's type.
Note: For
selectandmultiselectcolumns, option values are UUIDs (thekeyfield from the column'soptionsarray). Use the UUID, not the human-readable name.
// Text custom property
{"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$contains": "enterprise"}}
// Select custom property (use the option key UUID)
{"custom.a1b2c3d4-e5f6-7890-abcd-ef1234567890": "3e839b5c-b311-4887-b2da-727d2d75cdd6"}
// Multi-select custom property (use option key UUIDs)
{"custom.b2c3d4e5-f6a7-8901-bcde-f12345678901": {"$overlaps": ["a1b1c1d1-e1f1-1111-aaaa-111111111111", "b2b2c2d2-e2f2-2222-bbbb-222222222222"]}}
// Currency/number custom property
{"custom.c3d4e5f6-a7b8-9012-cdef-123456789012": {"$gte": 100000}}
// Date custom property
{"custom.d4e5f6a7-b8c9-0123-defa-234567890123": {"$gte": "2026-01-01"}}
// Boolean custom property
{"custom.e5f6a7b8-c9d0-1234-efab-345678901234": true}
// Check if custom property has a value
{"custom.f6a7b8c9-d0e1-2345-fabc-456789012345": {"$exists": true}}
Complex example
All top-level keys are ANDed together:
{
"name": {"$contains": "Zero"},
"location.city": "Helsinki",
"location.country": {"$in": ["United Kingdom", "Germany", "Sweden"]},
"value": {"$gte": 10000},
"closeDate": {"$gte": "2026-01-01", "$lte": "2026-03-31"},
"ownerIds": {"$includes": "<USER_UUID>"},
"stage": {"$in": ["<UUID_1>", "<UUID_2>"]},
"lastActivity": {"$exists": true},
"companyProfile.categories": {"$overlaps": ["Sales", "Marketing"]},
"custom.54e1ca7d-69c3-4b77-8266-8085b5834116": {"$contains": "enterprise"}
}
{"stage":"<PIPELINE_STAGE_UUID>"}Maximum number of records to return
100Pagination offset
0JSON string for sort order
{"name":"asc"}Successful response
Authentication failed or token is invalid/expired
GET /api/sequenceContactStatuses HTTP/1.1
Host: api.zero.inc
Authorization: Bearer YOUR_API_TOKEN
Accept: */*
{
"data": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"workspaceId": "123e4567-e89b-12d3-a456-426614174000",
"sequenceId": "123e4567-e89b-12d3-a456-426614174000",
"listId": "123e4567-e89b-12d3-a456-426614174000",
"contactId": "123e4567-e89b-12d3-a456-426614174000",
"status": "waitingForApproval",
"outcomeReason": "responded",
"outcomeStepId": "123e4567-e89b-12d3-a456-426614174000",
"responseCategorisation": "positive",
"errorMessage": "text",
"currentStepId": "123e4567-e89b-12d3-a456-426614174000",
"waitingOn": "delay",
"lastCompletedStepId": "123e4567-e89b-12d3-a456-426614174000",
"lastCompletedStepName": "text",
"nextActionAt": "2026-01-01T00:00:00.000Z",
"startedAt": "2026-01-01T00:00:00.000Z",
"endedAt": "2026-01-01T00:00:00.000Z",
"outcomeAt": "2026-01-01T00:00:00.000Z",
"lastUpdatedAt": "2026-01-01T00:00:00.000Z",
"archived": true,
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"total": 1
}Last updated

