Campaigns & Sequences

Campaigns

A campaign sends one email to a set of contacts, filtered by tags and properties.

Lifecycle

draft → sending → sent
         ↓  ↑
        paused

draft → scheduled → sending → sent
                      ↓
                   cancelled
StatusMeaning
draftCampaign is being configured. Can be edited.
scheduledWaiting for scheduledAt time. Can be cancelled.
sendingActively sending to recipients. Can be paused or cancelled.
pausedSending paused. Can be resumed.
sentAll emails have been processed.
cancelledCampaign was cancelled before completing.

Create a campaign

curl -X POST http://localhost:3891/v1/campaigns \
  -H "x-api-key: pgn_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "October Newsletter",
    "templateId": "monthly-newsletter",
    "templateData": {"month": "October", "highlight": "New API features"},
    "audienceFilter": {"tags": {"contains": "newsletter"}},
    "fromName": "Acme Updates",
    "fromEmail": "newsletter@yourdomain.com"
  }'
const campaign = await pigeons.campaigns.create({
  name: 'October Newsletter',
  templateId: 'monthly-newsletter',
  templateData: { month: 'October', highlight: 'New API features' },
  audienceFilter: { tags: { contains: 'newsletter' } },
  fromName: 'Acme Updates',
  fromEmail: 'newsletter@yourdomain.com',
})

Audience Filters

The audienceFilter object determines which contacts receive the campaign. Contacts where unsubscribed, bounced, or complained is true are always excluded.

Filter by tags

{
  "audienceFilter": {
    "tags": {
      "contains": "newsletter"
    }
  }
}
OperatorDescription
containsContact must have this exact tag
containsAnyContact must have at least one of these tags
containsAllContact must have all of these tags
{
  "tags": {
    "containsAll": ["newsletter", "active"],
    "containsAny": ["pro", "enterprise"]
  }
}

Filter by properties

{
  "audienceFilter": {
    "properties": {
      "plan": "pro",
      "region": "us"
    }
  }
}

Properties match on exact equality. All specified properties must match.

Combined filters

{
  "audienceFilter": {
    "tags": {"contains": "newsletter"},
    "properties": {"plan": "pro"}
  }
}

No filter

Omit audienceFilter to target all eligible contacts (those not unsubscribed, bounced, or complained).

Send a campaign

# Send immediately
curl -X POST http://localhost:3891/v1/campaigns/cmp_abc123.../send \
  -H "x-api-key: pgn_..."

# Schedule for later
curl -X POST http://localhost:3891/v1/campaigns/cmp_abc123.../send \
  -H "x-api-key: pgn_..." \
  -H "Content-Type: application/json" \
  -d '{"scheduledAt": "2024-06-01T09:00:00Z"}'

The campaign must have either a templateId or a subject. When sending, Pigeons:

  1. Queries all contacts matching the audience filter
  2. Excludes unsubscribed, bounced, and complained contacts
  3. Creates a job to send to each eligible contact
  4. Updates the stats.total field with the audience size

If no eligible contacts match, the send fails with a validation error.

Pause / Resume / Cancel

# Pause (only while sending)
curl -X POST http://localhost:3891/v1/campaigns/cmp_abc123.../pause \
  -H "x-api-key: pgn_..."

# Resume (only while paused)
curl -X POST http://localhost:3891/v1/campaigns/cmp_abc123.../resume \
  -H "x-api-key: pgn_..."

# Cancel (while sending or scheduled)
curl -X POST http://localhost:3891/v1/campaigns/cmp_abc123.../cancel \
  -H "x-api-key: pgn_..."

Campaign stats

const campaign = await pigeons.campaigns.get('cmp_abc123...')
console.log(campaign.stats)
{
  "total": 500,
  "sent": 450,
  "delivered": 440,
  "opened": 120,
  "bounced": 5,
  "complained": 1,
  "failed": 4
}

Stats update in real time as SES delivery events arrive. The opened count requires open tracking to be enabled.

Topic-scoped campaigns

Link a campaign to a subscription topic so contacts who unsubscribed from that specific topic are excluded:

const campaign = await pigeons.campaigns.create({
  name: 'Marketing Update',
  topicId: 'top_marketing...',
  templateId: 'marketing-update',
  audienceFilter: { tags: { contains: 'active' } },
})

Sequences

A sequence is a multi-step automation that sends a series of emails to individual contacts over time.

Create a sequence

curl -X POST http://localhost:3891/v1/sequences \
  -H "x-api-key: pgn_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Onboarding",
    "steps": [
      {"type": "email", "templateId": "welcome-email", "subject": "Welcome!"},
      {"type": "delay", "delayDuration": "1d"},
      {"type": "email", "templateId": "getting-started", "subject": "Getting Started"},
      {"type": "delay", "delayDuration": "3d"},
      {"type": "email", "templateId": "tips", "subject": "Pro Tips"}
    ]
  }'

Step types

TypeFieldsDescription
emailtemplateId, subject, templateData, topicIdSends an email using a template
delaydelayDurationWaits before proceeding. Format: 30m, 12h, 1d, 7d
conditionconditionEvaluates a condition before continuing

Sequence lifecycle

StatusMeaning
draftBeing configured. Can't enroll contacts yet.
activeRunning. Contacts can be enrolled.
pausedTemporarily stopped. Active enrollments are frozen.
archivedPermanently disabled.

Activate a sequence

A sequence must have at least one step before it can be activated:

curl -X POST http://localhost:3891/v1/sequences/seq_abc123.../activate \
  -H "x-api-key: pgn_..."

Enroll a contact

curl -X POST http://localhost:3891/v1/sequences/seq_abc123.../enroll \
  -H "x-api-key: pgn_..." \
  -H "Content-Type: application/json" \
  -d '{"contactId": "ctc_def456..."}'
const enrollment = await pigeons.sequences.enroll('seq_abc123...', 'ctc_def456...')
console.log(enrollment.status) // "active"

The sequence must be active. Enrollment creates a job to process the first step.

Enrollment statuses

StatusMeaning
activeCurrently progressing through steps
completedFinished all steps
exitedRemoved early (e.g. unsubscribed)
failedA step failed

Monitor enrollments

curl "http://localhost:3891/v1/sequences/seq_abc123.../enrollments?status=active" \
  -H "x-api-key: pgn_..."

Update a sequence

Pause the sequence first, then update:

curl -X POST http://localhost:3891/v1/sequences/seq_abc123.../pause \
  -H "x-api-key: pgn_..."

curl -X PATCH http://localhost:3891/v1/sequences/seq_abc123... \
  -H "x-api-key: pgn_..." \
  -H "Content-Type: application/json" \
  -d '{"steps": [...]}'

When steps are provided in an update, the entire step list is replaced.

Sequence stats

{
  "stats": {
    "enrolled": 150,
    "completed": 80,
    "exited": 10,
    "active": 60
  }
}

Per-step stats track sent, delivered, opened, bounced, and complained for each email step.