PushMail.dev

Sequences

Sequences are automated multi-step email flows with branching conditions. Define emails, delays, and condition branches, then enroll contacts to start the drip.

How sequences work

Contact enrolled

  ├─ Step 0: Send "Welcome" email          (immediately)

  ├─ Step 1: Wait 2 days                   (delay)

  ├─ Step 2: Send "Getting Started" email  (after delay)

  ├─ Step 3: Wait 5 days                   (delay)

  ├─ Step 4: Condition — opened step 2?
  │          ├─ TRUE  → Step 5
  │          └─ FALSE → Step 6

  ├─ Step 5: Send "Pro Tips" email         (engaged path)
  │          └─ Enrollment complete

  └─ Step 6: Send "Re-engagement" email    (unengaged path)
              └─ Enrollment complete

A cron job runs every minute to check for enrollments that need processing. If a contact unsubscribes, bounces, or is suppressed, the sequence stops automatically.

Cross-sequence send frequency cap

When a contact is enrolled in multiple sequences, you may want to avoid sending too many emails in a short period. Set a send frequency cap on the site to enforce a minimum number of days between sequence emails to the same contact, across all sequences on that site.

Set a 3-day frequency cap
curl -X PUT https://pushmail.dev/api/v1/sites/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sendFrequencyCapDays": 3}'

When the cap is hit, the enrollment is deferred (not skipped) — the cron worker re-checks every day until the cap window has passed, then sends the email and advances to the next step. Set to null to disable.

Note: The frequency cap only applies to sequence emails. Transactional sends (POST /v1/send) and campaigns are not affected.

To see which contacts are currently enrolled in multiple sequences for a site, use the multi-sequence report:

List contacts in multiple sequences
curl "https://pushmail.dev/api/v1/reports/multi-sequence?siteId=1" \
  -H "Authorization: Bearer pm_live_YOUR_KEY"

This is also available in the dashboard under Reports.

Create a sequence

POST /v1/sequences

Request
curl -X POST https://pushmail.dev/api/v1/sequences \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": 1,
    "name": "Onboarding Drip",
    "triggerType": "manual",
    "sendingConfigId": 2
  }'
Response
{
  "data": {
    "id": 1,
    "siteId": 1,
    "name": "Onboarding Drip",
    "status": "draft",
    "triggerType": "manual",
    "createdAt": "2025-01-15T10:00:00.000Z"
  }
}
ParameterTypeRequiredDescription
siteIdintegerYesThe site to create the sequence in
namestringYesSequence name
triggerTypestringYes"manual", "list_add", "tag_add", or "segment_enter"
sendingConfigIdintegerRequired to activateSending config to use for all emails in this sequence. Can be set at creation or before activation. See Sending for setup.

Trigger types

TypeDescription
manualEnroll contacts via API or dashboard
list_addAuto-enroll when a contact is added to a specific list
tag_addAuto-enroll when a contact receives a specific tag
segment_enterAuto-enroll when a contact enters a dynamic segment

For auto-enrollment triggers, set triggerConfig as a JSON string on the sequence:

  • list_add: set triggerConfig: "{\"listId\":5}" to auto-enroll contacts when they are added to list 5
  • tag_add: set triggerConfig: "{\"tagId\":3}" to auto-enroll contacts when tag 3 is applied
  • segment_enter: set triggerConfig: "{\"segmentId\":1}" to auto-enroll contacts when they match segment 1

You can also set an optional goalConfig (JSON string) for conversion tracking (e.g. to end the sequence early when a contact reaches a goal).

Add steps

POST /v1/sequences/:seqId/steps

Steps execute in order by position. Three step types: email, delay, and condition.

Add steps with a condition branch
curl -X POST https://pushmail.dev/api/v1/sequences/1/steps \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "steps": [
      { "position": 0, "type": "email", "templateId": 5 },
      { "position": 1, "type": "delay", "delayMinutes": 4320 },
      {
        "position": 2,
        "type": "condition",
        "conditionConfig": "{\"type\":\"engagement_check\",\"metric\":\"opened\",\"stepOrder\":0,\"operator\":\"did\"}",
        "trueBranchStepOrder": 3,
        "falseBranchStepOrder": 4
      },
      { "position": 3, "type": "email", "templateId": 6 },
      { "position": 4, "type": "email", "templateId": 7 }
    ]
  }'

Step fields

FieldTypeRequiredDescription
positionintegerYesStep order (0-based)
typestringYes"email", "delay", or "condition"
templateIdintegerEmail stepsTemplate to send
delayMinutesintegerDelay stepsMinutes to wait
conditionConfigstring (JSON)Condition stepsCondition rule to evaluate (see below)
trueBranchStepOrderinteger or nullCondition stepsPosition to jump to if condition is true. Default: next step
falseBranchStepOrderinteger or nullCondition stepsPosition to jump to if condition is false. Default: complete sequence

Batch limit: A maximum of 200 steps can be created per request. If you need more steps, split them across multiple API calls.

Common delay values: 60 (1 hour), 1440 (1 day), 4320 (3 days), 10080 (1 week), 43200 (30 days)

Condition steps

Condition steps evaluate a rule against the enrolled contact's data and route the enrollment down different paths. The conditionConfig field is a JSON string with the condition rule.

Supported condition types

Tag check

Check if a contact has or does not have a specific tag.

{
  "type": "tag_check",
  "tagId": 5,
  "operator": "has"
}
FieldValuesDescription
tagIdintegerThe tag ID to check
operator"has" or "not_has"Whether the contact should have the tag

Field check

Check a contact's field value (top-level fields or custom fields).

{
  "type": "field_check",
  "field": "customFields.plan_type",
  "operator": "equals",
  "value": "pro"
}
FieldValuesDescription
fieldstringField path. Use customFields.key for custom fields, or a top-level field name like firstName, company, etc.
operator"equals", "not_equals", "contains", "exists"Comparison operator
valuestring (optional)Value to compare against. Not needed for "exists"

Engagement check

Check if a contact opened or clicked a specific previous step's email.

{
  "type": "engagement_check",
  "metric": "opened",
  "stepOrder": 0,
  "operator": "did"
}
FieldValuesDescription
metric"opened" or "clicked"What engagement to check
stepOrderintegerPosition of the email step to check
operator"did" or "did_not"Whether the contact should have engaged

Event check

Check if a contact has a specific event type in their send history.

{
  "type": "event_check",
  "eventName": "purchased",
  "operator": "performed",
  "withinDays": 7
}
FieldValuesDescription
eventNamestringThe event type to look for (e.g. "delivered", "open", "click")
operator"performed" or "not_performed"Whether the event should have occurred
withinDaysinteger (optional)Only check events within this many days

Branching behavior

When a condition step is evaluated:

  1. If TRUE and trueBranchStepOrder is set, the enrollment jumps to that step position
  2. If TRUE and trueBranchStepOrder is null, the enrollment advances to the next sequential step
  3. If FALSE and falseBranchStepOrder is set, the enrollment jumps to that step position
  4. If FALSE and falseBranchStepOrder is null, the enrollment completes (sequence ends)

The condition evaluation is logged in the enrollment step logs with a status of "condition_true" or "condition_false".

Activate a sequence

Sequences start in draft status. A sendingConfigId is required before activation — this tells PushMail which from address and provider to use for all emails in the sequence.

Activate (sendingConfigId must already be set)
curl -X PUT https://pushmail.dev/api/v1/sequences/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'
Set sendingConfigId and activate in one call
curl -X PUT https://pushmail.dev/api/v1/sequences/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active", "sendingConfigId": 2}'

You can pause a sequence at any time. Active enrollments will stop processing until you reactivate.

Pause a sequence
curl -X PUT https://pushmail.dev/api/v1/sequences/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

Auto BCC

You can set an autoBcc address on a sequence to automatically BCC every email sent through it. This is useful for CRM integration, compliance archiving, or internal monitoring.

Set auto BCC on a sequence
curl -X PUT https://pushmail.dev/api/v1/sequences/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"autoBcc": "archive@yourcompany.com"}'

If no sequence-level autoBcc is set, the site-level autoBcc is used (if configured via PUT /sites/:id). Set autoBcc to null to clear it.

Enroll a contact

POST /v1/sequences/:id/enroll

Enrolling contacts starts them at step 0 by default. Each contact can only be enrolled once per sequence (duplicates are rejected). You can enroll 1 to 100 contacts per request.

Use startAtStep to begin at a specific step position — useful for migrating subscribers who are already mid-drip from another system.

ParameterTypeDescription
contactIdsrequirednumber[]Array of contact IDs to enroll (1–100)
startAtStepnumberStep position to start at (0-indexed). Defaults to 0 (first step).
Request
curl -X POST https://pushmail.dev/api/v1/sequences/1/enroll \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactIds": [42] }'
Start at step 3 (skip earlier steps)
curl -X POST https://pushmail.dev/api/v1/sequences/1/enroll \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactIds": [42], "startAtStep": 3 }'
Response
{
  "data": {
    "enrolled": 1
  }
}

Full example: Welcome series

Here's how to set up a 3-email welcome sequence and enroll new signups automatically from your app:

setup-sequence.sh
API="https://pushmail.dev/api/v1"
KEY="pm_live_YOUR_KEY"

# 1. Create the sequence
SEQ=$(curl -s -X POST "$API/sequences" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"siteId": 1, "name": "Welcome Series", "triggerType": "manual"}' \
  | jq -r '.data.id')

# 2. Add steps: email → delay → email → delay → email
curl -X POST "$API/sequences/$SEQ/steps" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "steps": [
      {"position": 1, "type": "email", "templateId": 1},
      {"position": 2, "type": "delay", "delayMinutes": 2880},
      {"position": 3, "type": "email", "templateId": 2},
      {"position": 4, "type": "delay", "delayMinutes": 7200},
      {"position": 5, "type": "email", "templateId": 3}
    ]
  }'

# 3. Activate
curl -X PUT "$API/sequences/$SEQ" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

echo "Sequence $SEQ is live!"
In your app (after user signs up)
// After creating the user in your database...
const contact = await fetch("https://pushmail.dev/api/v1/contacts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.PUSHMAIL_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    siteId: 1,
    email: user.email,
    firstName: user.name,
    tags: ["signup"],
  }),
}).then(r => r.json());

// Enroll in welcome sequence
await fetch(`https://pushmail.dev/api/v1/sequences/${WELCOME_SEQ_ID}/enroll`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.PUSHMAIL_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ contactIds: [contact.data.id] }),
});

Sequence Playbooks

Playbooks are pre-built sequence templates that you can deploy with a single API call. Instead of manually creating a sequence, adding templates, and wiring up steps, just pick a playbook and go.

List available playbooks

GET /v1/sequences/playbooks

No authentication required. Returns all available playbook templates with metadata.

Request
curl https://pushmail.dev/api/v1/sequences/playbooks
Response
{
  "data": {
    "playbooks": [
      {
        "id": "welcome-series",
        "name": "Welcome Series",
        "description": "Introduce your brand and set expectations for new subscribers...",
        "category": "onboarding",
        "recommendedTrigger": "list_add",
        "suggestedTriggerValue": "new-subscribers",
        "stepCount": 3,
        "steps": [
          { "type": "email", "emailSubject": "Welcome to {{company}}!" },
          { "type": "delay", "delayMinutes": 2880 },
          { "type": "email", "emailSubject": "Getting started with {{company}}" }
        ]
      }
    ]
  }
}

Available playbooks

IDNameCategoryStepsDescription
welcome-seriesWelcome Seriesonboarding3Brand intro + feature highlights after 2 days
onboarding-dripOnboarding Driponboarding5Quick start + two feature spotlights over several days
re-engagementRe-engagement / Win-backretention3Incentive-driven win-back with final reminder
post-purchasePost-Purchase Follow-upsales4Thank you, usage tips, and review request over 10 days
newsletter-nurtureNewsletter Nurtureengagement3Subscription confirmation + best content roundup
product-launchProduct Launchsales4Teaser, launch announcement, and social proof

Create a sequence from a playbook

POST /v1/sequences/playbooks/:playbookId/create

Auth required. Creates a complete sequence with templates and steps from the chosen playbook.

Request
curl -X POST https://pushmail.dev/api/v1/sequences/playbooks/welcome-series/create \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": 1,
    "name": "My Welcome Series"
  }'
Response
{
  "data": {
    "sequence": {
      "id": 5,
      "siteId": 1,
      "name": "My Welcome Series",
      "status": "draft",
      "triggerType": "list_add",
      "triggerConfig": "{\"value\":\"new-subscribers\"}"
    },
    "steps": [
      { "id": 10, "position": 1, "type": "email", "templateId": 22, "templateName": "My Welcome Series — Welcome to {{company}}!" },
      { "id": 11, "position": 2, "type": "delay", "delayMinutes": 2880 },
      { "id": 12, "position": 3, "type": "email", "templateId": 23, "templateName": "My Welcome Series — Getting started with {{company}}" }
    ],
    "playbook": {
      "id": "welcome-series",
      "name": "Welcome Series"
    }
  }
}
ParameterTypeDescription
siteIdrequirednumberSite to create the sequence in
namestringCustom name (defaults to the playbook name)
sendingConfigIdnumberSending config to use for the sequence

The sequence is created in draft status. Customize the generated templates, then activate the sequence when ready. All email templates use {{first_name}}, {{company}}, and {{unsubscribe_url}} variables that get replaced at send time.

Quick start: playbook to live sequence in 2 API calls

Deploy a welcome series in 2 calls
API="https://pushmail.dev/api/v1"
KEY="pm_live_YOUR_KEY"

# 1. Create from playbook (creates sequence + templates + steps)
SEQ=$(curl -s -X POST "$API/sequences/playbooks/welcome-series/create" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"siteId": 1, "name": "Welcome Series"}' \
  | jq -r '.data.sequence.id')

# 2. Activate
curl -X PUT "$API/sequences/$SEQ" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

echo "Sequence $SEQ is live!"

Holiday email injection

Inject holiday-specific emails into active sequences without disrupting anyone's position. On the scheduled date, every active enrollment receives the holiday email before their next normal step, then resumes exactly where they left off.

This works regardless of where each contact is in the sequence — some may be on step 2, others on step 8. Everyone gets the holiday email, then continues their normal flow.

Schedule a holiday

POST /v1/holidays

ParameterTypeDescription
siteIdrequirednumberSite to schedule the holiday for
namerequiredstringHoliday name (e.g. "Black Friday 2026")
templateIdrequirednumberTemplate to send as the holiday email
daterequiredstringDate to send (YYYY-MM-DD format)
sequenceIdsnumber[]Limit to specific sequence IDs. Omit to apply to all active sequences on the site.
sendingConfigIdnumberOverride sending config. Falls back to each sequence's own config.
Schedule a Black Friday holiday email
curl -X POST https://pushmail.dev/api/v1/holidays \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": 1,
    "name": "Black Friday 2026",
    "templateId": 15,
    "date": "2026-11-27"
  }'
Response
{
  "data": {
    "id": 1,
    "siteId": 1,
    "name": "Black Friday 2026",
    "templateId": 15,
    "date": "2026-11-27",
    "status": "scheduled",
    "sentCount": 0
  }
}

Scope to specific sequences

By default, a holiday email applies to all active sequences on the site. To limit it to specific sequences:

Only inject into sequences 1 and 3
curl -X POST https://pushmail.dev/api/v1/holidays \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": 1,
    "name": "Christmas Sale",
    "templateId": 20,
    "date": "2026-12-25",
    "sequenceIds": [1, 3]
  }'

How it works

  1. On the scheduled date, the cron job checks each active enrollment before processing its normal step
  2. If a holiday email is due and hasn't been sent to that enrollment yet, it sends the holiday email
  3. Normal step processing is deferred by 1 hour to avoid double-sending
  4. On the next cron tick, the enrollment resumes its normal sequence from where it left off
  5. After the date passes, the holiday is automatically marked as completed

Holiday statuses

StatusDescription
scheduledCreated but the date hasn't arrived yet
activeCurrently sending to enrollments (date is today)
completedDate has passed, all sends finished
cancelledManually cancelled before completion

List holidays

GET /v1/holidays?siteId=1

List holidays
curl "https://pushmail.dev/api/v1/holidays?siteId=1" \
  -H "Authorization: Bearer pm_live_YOUR_KEY"

Cancel a holiday

PUT /v1/holidays/:id

Cancel a scheduled holiday
curl -X PUT https://pushmail.dev/api/v1/holidays/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "cancelled"}'

Delete a holiday

DELETE /v1/holidays/:id

Delete a holiday and its send records
curl -X DELETE https://pushmail.dev/api/v1/holidays/1 \
  -H "Authorization: Bearer pm_live_YOUR_KEY"

Cancel enrollments

DELETE /v1/sequences/:id/enrollments

Cancel one or more active enrollments. Supports three modes: single (query param), bulk (JSON body with IDs), and by contact ID.

Cancel a single enrollment (query param — legacy)
curl -X DELETE "https://pushmail.dev/api/v1/sequences/1/enrollments?enrollment_id=42" \
  -H "Authorization: Bearer pm_live_YOUR_KEY"
Bulk cancel by enrollment IDs (up to 100)
curl -X DELETE https://pushmail.dev/api/v1/sequences/1/enrollments \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enrollmentIds": [42, 43, 44]}'
Bulk response
{
  "data": {
    "cancelled": 3,
    "total": 3
  }
}
Cancel by contact ID
curl -X DELETE https://pushmail.dev/api/v1/sequences/1/enrollments \
  -H "Authorization: Bearer pm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contactId": 19}'
Contact ID response
{
  "data": {
    "cancelled": true,
    "contactId": 19
  }
}

Enrollment statuses

StatusDescription
activeCurrently progressing through steps
completedAll steps processed
cancelledManually cancelled or contact unsubscribed
failedProcessing error (contact bounced, etc.)

Next steps

  • Segments -- Use dynamic audiences to trigger sequence enrollment
  • Campaigns -- Send one-off broadcasts to a list, tag, or segment
  • Webhooks -- Track delivery, opens, and clicks in real-time
  • API Reference -- Complete endpoint documentation

On this page