Skip to main content

Sequences

Sequences are multi-step outreach workflows: a series of email (and optional LinkedIn / phone / SMS / manual-task) steps with delays and conditional branching, sent on behalf of one or more connected mailboxes. Use the API to build sequences programmatically, enroll contacts, monitor analytics, and rotate sending across multiple mailboxes.

tip

Sequence API endpoints require the Starter plan or higher. Sequences send through your connected Send Accounts — connect at least one before enrolling contacts.

Lifecycle at a glance

A sequence moves through draftactivepaused. While active, the sequence worker processes enrollments, sends step emails on schedule, and writes events for analytics. Pausing halts new sends but keeps enrollments queryable. Replies land in the Unified Inbox.


List Sequences

Endpoint

GET /v1/sequences

Headers

HeaderValue
AuthorizationBearer cdb_your_api_key

Example Request

curl -X GET \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/sequences"

Response

{
"sequences": [
{
"id": 41,
"name": "Q2 outbound — fintech",
"description": "Six-step sequence for Series A fintechs in EU",
"from_name": "Alex from CronDB",
"from_email": "alex@crondb.com",
"status": "active",
"daily_send_limit": 80,
"warmup_enabled": true,
"warmup_start_volume": 10,
"warmup_increment": 5,
"warmup_started_at": "2026-04-15T08:00:00Z",
"goal_type": "reply",
"goal_url": null,
"auto_pause_bounce_rate": 5.0,
"enable_unsubscribe": true,
"step_count": 6,
"enrollment_count": 412,
"created_at": "2026-04-12T10:14:00Z",
"updated_at": "2026-05-04T19:28:11Z"
}
],
"count": 1,
"limit": 50,
"remaining": 49
}

limit and remaining are plan-tier sequence quotas, not pagination — the list returns every sequence on the account.


Create Sequence

Endpoint

POST /v1/sequences

Body Parameters

ParameterTypeRequiredDescription
namestringYes1–200 chars.
descriptionstringNoUp to 1000 chars.
from_namestringNoDisplay name on outbound.
from_emailstringNoFrom address. Must match a connected send account.
daily_send_limitintegerNo1–10000, default 50.
warmup_enabledbooleanNoDefault false. When true, sends ramp from warmup_start_volume by warmup_increment per day.
warmup_start_volumeintegerNoDefault 10.
warmup_incrementintegerNoDefault 5.
settingsobjectNoFree-form JSON for sequence-level toggles (e.g. quiet hours, send window).

Example Request

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Q2 outbound — fintech",
"from_email": "alex@crondb.com",
"daily_send_limit": 80,
"warmup_enabled": true
}' \
"https://api.crondb.com/v1/sequences"

The response is the new sequence object (status draft). Add steps next.


Get / Update / Delete Sequence

MethodPathNotes
GET/v1/sequences/{sequence_id}Returns the sequence plus steps[] and the most-recent 100 enrollments[].
PUT/v1/sequences/{sequence_id}Same body fields as create; all optional.
DELETE/v1/sequences/{sequence_id}Removes the sequence and cascades to steps + enrollments.

Activate / Pause

POST /v1/sequences/{sequence_id}/activate
POST /v1/sequences/{sequence_id}/pause

activate requires the sequence to have at least one actionable step (email, linkedin_connect, linkedin_message, phone_call, manual_task, or sms). Returns 400 if there are zero actionable steps.

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
"https://api.crondb.com/v1/sequences/41/activate"

Clone Sequence

Duplicate a sequence and all its steps (status draft, prefixed with "Copy of "). Conditional condition_step_id references are remapped to the new step IDs.

POST /v1/sequences/{sequence_id}/clone

Add / Update / Delete Steps

GET    /v1/sequences/{sequence_id}/steps
POST /v1/sequences/{sequence_id}/steps
PUT /v1/sequences/{sequence_id}/steps/{step_id}
DELETE /v1/sequences/{sequence_id}/steps/{step_id}

Step Body

ParameterTypeDescription
step_typestringemail, delay, linkedin_connect, linkedin_message, phone_call, manual_task, or sms.
delay_daysintegerWait time relative to the previous step (>= 0).
delay_hoursinteger0–23 hours, added to delay_days.
subjectstringEmail subject (when step_type=email). Supports template tokens — see below.
body_htmlstringEmail body. Supports {{first_name}}, {{domain}}, {{ai_write:...}}, etc.
variant_b_subjectstringOptional A/B variant.
variant_b_body_htmlstringOptional A/B variant body.
ab_test_enabledbooleanToggle 50/50 A/B sending.
condition_typestringConditional branching: replied, clicked, opened, etc.
condition_step_idintegerStep whose event the condition checks against.
variantsarray<object>Multivariate test: [{label, subject, body_html, weight}], weights as percentages.

Example: Add an email step

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"step_type": "email",
"delay_days": 3,
"delay_hours": 0,
"subject": "Following up on {{domain}}",
"body_html": "<p>Hi {{first_name}},</p><p>{{ai_write: a one-line opener referencing the recipient industry}}</p>"
}' \
"https://api.crondb.com/v1/sequences/41/steps"

Enroll Contacts

Enroll one or more contacts into an active sequence. The first step's send time is calculated from delay_days/delay_hours relative to enrollment.

Endpoint

POST /v1/sequences/{sequence_id}/enroll

Rate Limit

10 enroll requests per user per minute. For larger imports, batch up to several hundred contacts per request or use the CSV bulk endpoint at POST /v1/sequences/{id}/enroll-csv.

Body Parameters

ParameterTypeRequiredDescription
contactsarray<object>YesAt least one.
contacts[].domainstringYesCompany domain. Used to look up enrichment data for templating.
contacts[].contact_emailstringYesRecipient email.
contacts[].metadataobjectNoFree-form JSON merged into the enrollment row, available to template tokens.

Example Request

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{"domain": "acme.com", "contact_email": "jordan@acme.com", "metadata": {"first_name": "Jordan"}},
{"domain": "globex.com", "contact_email": "sam@globex.com", "metadata": {"first_name": "Sam"}}
]
}' \
"https://api.crondb.com/v1/sequences/41/enroll"

Response

{
"enrolled": 2,
"skipped": 0,
"skipped_emails": []
}

skipped_emails contains the original recipient address of every contact that wasn't enrolled. Contacts are skipped (no error) when:

  • The email fails validation (malformed, role-based, etc.)
  • The contact is already actively enrolled in this sequence
  • The email matches an entry on the suppression list
  • A concurrent enroll request has the same (sequence_id, contact_email) lock held

List Enrollments

GET /v1/sequences/{sequence_id}/enrollments
Query ParameterDescription
statusFilter by active, completed, bounced, unsubscribed, replied, paused.
limitDefault 50.
offsetDefault 0.

Each enrollment carries its current step, next send time, engagement score, lead temperature (cold/warm/hot), and any goal-completion metadata.

Per-Enrollment Controls

MethodPathEffect
POST/v1/sequences/{id}/enrollments/{enrollment_id}/pauseHalt sends for this contact only.
POST/v1/sequences/{id}/enrollments/{enrollment_id}/resumeResume from where it stopped.
DELETE/v1/sequences/{id}/enrollments/{enrollment_id}Drop the enrollment entirely.

Sequence Analytics

Aggregate funnel + per-step stats. The funnel is sourced from real sequence_events rows, not modelled estimates.

Endpoint

GET /v1/sequences/{sequence_id}/analytics

Response

{
"sequence_id": 41,
"total_enrolled": 412,
"active": 188,
"completed": 167,
"bounced_enrollments": 14,
"unsubscribed_enrollments": 8,
"replied_enrollments": 35,
"goal_completed": 12,
"rollup": {
"sent": 1842,
"delivered": 1801,
"opened": 1043,
"clicked": 188,
"replied": 41,
"bounced": 14,
"unsubscribed": 8
},
"interested_replies": 19,
"steps": [
{
"step_id": 87,
"position": 1,
"step_type": "email",
"subject": "Quick question about {{domain}}'s pipeline",
"sent": 412,
"delivered": 405,
"opened": 247,
"clicked": 41,
"replied": 18,
"bounced": 7,
"unsubscribed": 2,
"open_rate": 60.0,
"click_rate": 10.0
}
]
}

Response Fields

FieldDescription
total_enrolled / active / completed / etc.Counts at the enrollment level.
rollupSum of sequence_events across every step. Reflects observed sends/opens/clicks/replies/bounces/unsubscribes — not modelled estimates.
interested_repliesReplies the inbox classifier tagged as interested. Replaces the previous fabricated "Booked = replied × 0.25" placeholder.
steps[].open_rate / click_rateComputed as event count ÷ sent × 100, rounded to 1 decimal.

Mailbox Pool (Multi-Mailbox Rotation)

A sequence can rotate sending across a pool of connected mailboxes (round-robin), respecting each account's daily limit and skipping accounts that have hit per-mailbox bounce thresholds. Setting an empty pool ([]) reverts to single-mailbox mode using sequence.send_account_id.

Get the pool

GET /v1/sequences/{sequence_id}/mailbox-pool
{
"sequence_id": 41,
"pool": [
{
"id": 9,
"send_account_id": 12,
"from_name": "Alex from CronDB",
"from_email": "alex@crondb.com",
"is_active": true,
"position": 0,
"daily_limit": 80,
"sent_today": 17,
"last_used_at": "2026-05-05T13:42:00Z"
}
]
}

Replace the pool

PUT /v1/sequences/{sequence_id}/mailbox-pool

Body:

{ "send_account_ids": [12, 18, 24] }

position is the index in the array (rotation order). Send accounts the caller doesn't own are silently dropped. The replacement is atomic — the entire pool is rewritten in one transaction.

curl -X PUT \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"send_account_ids": [12, 18, 24]}' \
"https://api.crondb.com/v1/sequences/41/mailbox-pool"

Response:

{ "sequence_id": 41, "pool_size": 3 }

Test Send & Preview

Before activating, you can render a step against synthetic data without touching enrollments:

MethodPathDescription
POST/v1/sequences/{id}/test-sendSend a real email of step step_position to a chosen address. Rate-limited to 5/min. Doesn't create events.
POST/v1/sequences/{id}/previewRender the step body and subject as the engine would, returning HTML + tokens used.
POST/v1/sequences/email-healthScore a subject/body pair for spam triggers, all-caps ratio, link density, etc. (no DB write).


Suppression List

Per-account list of emails that should never be enrolled, regardless of sequence. Bounces, unsubscribes, and complaints add automatically; you can also seed it manually.

List

GET /v1/sequences/suppression?limit=50&offset=0
{
"entries": [
{
"id": 314,
"email": "no@thanks.com",
"reason": "unsubscribe",
"domain": "thanks.com",
"created_at": "2026-04-22T11:14:00Z"
}
],
"total": 47
}

Add one

POST /v1/sequences/suppression
Body FieldRequiredDescription
emailYesRecipient address. Lowercased.
reasonNoOne of manual, bounce, unsubscribe, complaint, invalid. Defaults to manual.

Idempotent — re-adding an existing email returns the existing entry.

Bulk import

POST /v1/sequences/suppression/import
{ "emails": ["a@example.com", "b@example.com"] }

Returns {added: int, skipped: int} — duplicates and malformed addresses are silently skipped.

Remove

DELETE /v1/sequences/suppression/{entry_id}

AI Email Writer

Generate a subject + body draft from a structured prompt. Used by the dashboard "Write with AI" button when authoring steps.

Endpoint

POST /v1/sequences/ai-writer

Body Parameters

FieldTypeRequiredDescription
purposestringYescold_outreach, follow_up, re_engagement, partnership, or event_invite.
company_namestringNoSender brand name (used in opener + signature).
industrystringNoRecipient industry — sharpens the angle.
product_descriptionstringNoOne-line pitch fed into the prompt.
tonestringNoprofessional (default), casual, friendly, urgent.
lengthstringNoshort, medium (default), long.
include_ctabooleanNoDefault true.
cta_typestringNoschedule_call, reply, visit_link, sign_up.
additional_contextstringNoExtra instructions appended verbatim to the prompt.

Example Request

curl -X POST \
-H "Authorization: Bearer cdb_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"purpose": "cold_outreach",
"company_name": "CronDB",
"industry": "fintech",
"tone": "professional",
"length": "short",
"cta_type": "schedule_call"
}' \
"https://api.crondb.com/v1/sequences/ai-writer"

Response

{
"subject": "Question about how {{company}} handles fresh-domain intent",
"body_html": "<p>Hi {{first_name}},</p><p>...</p>",
"tokens_used": 380,
"model": "deepseek-chat"
}

The body comes back with template tokens ({{first_name}}, {{company}}, {{domain}}) intact — drop it directly into a step's body_html.


A/B Test Results

Two-variant comparison between subject / body_html (variant A) and variant_b_subject / variant_b_body_html (variant B) on a step.

Endpoint

GET /v1/sequences/{sequence_id}/steps/{step_id}/ab-results

Response

{
"step_id": 87,
"ab_test_enabled": true,
"ab_winner": null,
"variants": {
"a": { "sent": 320, "opened": 184, "clicked": 22, "open_rate": 57.5, "click_rate": 6.9 },
"b": { "sent": 318, "opened": 211, "clicked": 35, "open_rate": 66.4, "click_rate": 11.0 }
}
}

Pick a winner

POST /v1/sequences/{sequence_id}/steps/{step_id}/ab-winner
{ "winner": "b" }

Once a winner is locked, the worker stops splitting and sends 100% of the variant from that point forward.


Multivariate Test Results

For steps with variants: [{label, subject, body_html, weight}] configured (more than two variants), use this endpoint instead of /ab-results.

GET /v1/sequences/{sequence_id}/steps/{step_id}/multivariate-results
{
"step_id": 87,
"variants": [
{ "label": "A", "subject": "...", "weight": 25 },
{ "label": "B", "subject": "...", "weight": 25 },
{ "label": "C", "subject": "...", "weight": 25 },
{ "label": "D", "subject": "...", "weight": 25 }
],
"results": {
"A": { "sent": 124, "opened": 72, "clicked": 9, "replied": 2, "open_rate": 58.1, "click_rate": 7.3, "reply_rate": 1.6, "composite_score": 23.0 },
"B": { "sent": 130, "opened": 89, "clicked": 18, "replied": 5, "open_rate": 68.5, "click_rate": 13.8, "reply_rate": 3.8, "composite_score": 26.2 }
},
"leading_variant": "B",
"has_sufficient_data": true
}

composite_score = open_rate × 0.3 + click_rate × 0.3 + reply_rate × 0.4. leading_variant is the highest score with sent ≥ 5 (a guard against early-stage noise). has_sufficient_data is true when every variant has shipped at least 10 messages — useful as a gate before declaring a winner.


Engagement Score History

Per-enrollment timeline of engagement-score changes. Powers the contact-detail sparkline in the dashboard.

Endpoint

GET /v1/sequences/{sequence_id}/enrollments/{enrollment_id}/score-history?limit=50

limit accepts 1–200, default 50. Results come back in chronological order (oldest first).

Response

{
"enrollment_id": 5621,
"current_score": 42,
"current_temperature": "warm",
"history": [
{ "score": 0, "temperature": "cold", "event_type": "enrolled", "created_at": "2026-04-12T10:00:00Z" },
{ "score": 5, "temperature": "cold", "event_type": "delivered", "created_at": "2026-04-12T10:05:00Z" },
{ "score": 15, "temperature": "cold", "event_type": "opened", "created_at": "2026-04-12T14:22:00Z" },
{ "score": 32, "temperature": "warm", "event_type": "clicked", "created_at": "2026-04-13T09:18:00Z" },
{ "score": 42, "temperature": "warm", "event_type": "replied", "created_at": "2026-04-13T11:42:00Z" }
]
}

Lead temperature transitions: coldwarm at score ≥ 25, warmhot at score ≥ 60.


Notes

  • Templating: {{first_name}}, {{domain}}, plus enrichment fields (industry, business_type, country) work out of the box. {{ai_write: instruction}} calls your connected BYOK LLM at send time.
  • Engagement scoring runs on every event — track-able at GET /v1/sequences/{id}/enrollments/{eid}/score-history.
  • Bounce auto-pause: when a sequence's bounce rate exceeds auto_pause_bounce_rate (default 5%), the sequence is auto-paused. With multi-mailbox rotation, a single mailbox hitting the threshold is removed from the pool while the sequence keeps running on the rest.
  • The webhook events enrollment.completed, step.replied, step.bounced, etc. fire as the sequence runs — see Webhooks.

Next Steps