PigeonAtlas

Sequences

A sequence is a few emails to one person who joined a list, each a set time after they joined: the follow-up to a free guide, the first two weeks of a course. Your app adds the person with the consent they gave, defines the sequence in its own code, and enrolls them. We send each step on the newsletter lane, with a working unsubscribe, and stop the moment they unsubscribe, complain or buy.

Only people who asked

A sequence can only reach someone subscribed to its list, and the API only subscribes someone with a consent record: where they signed up, the words they agreed to, and when. Cold outreach, bought lists and scraped lists are not permitted, by sequence or any other way. Acceptable Use Policy

Before you start

  1. A verified newsletter domain, such as news.yourcompany.com, added as "Newsletters". A sequence from a transactional domain is refused, so a complaint about a lesson can never reach your password resets.
  2. A list. Create it under Newsletters, Lists. Its page shows the slug your code uses, such as acme-guides.
  3. An API key that may do it. When you create the key, tick Add contacts (contacts:write) and Run sequences (sequences:write). Untick Send email if this key is for nothing else. A key issued for one brand uses that brand's list when a request names none, and sees only that brand's sequences.
  4. Growth or Scale. Sequences are newsletter mail, and each step counts as one email toward your allowance.

1. Add the person, with their consent

PUT https://pigeonatlas.com/v1/contacts adds or updates a person on a list. consent is required: source (where they signed up), text (the words next to the checkbox or button), at (ISO 8601) and, if you have it, ip. Without it the request is refused. Each distinct consent is kept as a record you can show if anyone asks.

Request
curl -X PUT https://pigeonatlas.com/v1/contacts \
  -H "Authorization: Bearer $PIGEONATLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "name": "Jane Doe",
    "audience": "acme-guides",
    "consent": {
      "source": "guide:pricing-teardown",
      "text": "Email me the guide and five short lessons about pricing. I can unsubscribe at any time.",
      "at": "2026-10-15T09:30:00Z",
      "ip": "203.0.113.7"
    },
    "tags": ["guide:pricing-teardown"]
  }'
Response
{
  "object": "contact",
  "email": "jane@example.com",
  "name": "Jane Doe",
  "tags": ["guide:pricing-teardown"],
  "status": "subscribed",
  "audience": { "id": 12, "slug": "acme-guides", "name": "Acme guides" },
  "audiences": [
    { "id": 12, "slug": "acme-guides", "name": "Acme guides", "status": "subscribed",
      "subscribed_at": "2026-10-15T09:30:02Z", "unsubscribed_at": null }
  ],
  "suppressed": null,
  "enrollments": [],
  "created_at": "2026-10-15T09:30:02Z",
  "updated_at": "2026-10-15T09:30:02Z",
  "created": true
}

2. Define the sequence

PUT https://pigeonatlas.com/v1/sequences/:key with the whole definition. Keep it in your code and send it on every deploy: the key is yours to choose, and sending the same definition again changes nothing.

Request
curl -X PUT https://pigeonatlas.com/v1/sequences/pricing-guide \
  -H "Authorization: Bearer $PIGEONATLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pricing guide follow-up",
    "audience": "acme-guides",
    "from": "Acme <hello@news.acme.com>",
    "reply_to": "team@acme.com",
    "quiet_hours": { "start": 21, "end": 8, "time_zone": "America/New_York" },
    "steps": [
      { "key": "welcome", "delay_hours": 0,
        "subject": "Your pricing guide, {{name|there}}",
        "html": "<p>Hi {{name|there}}, here is {{data.guide_title}}.</p><p><a href=\"{{unsubscribe_url}}\">Unsubscribe</a></p>",
        "text": "Hi {{name|there}}, here is {{data.guide_title}}.\n\nUnsubscribe: {{unsubscribe_url}}" },
      { "key": "anchor-price", "delay_hours": 48,
        "subject": "The first number a buyer sees",
        "text": "One lesson, one example, one next step." },
      { "key": "office-hours", "delay_hours": 120,
        "subject": "Ask us about your pricing page",
        "text": "Book a slot, or reply to this email." }
    ]
  }'

3. Enroll them

Right after adding them. Only a contact subscribed to the sequence's list can be enrolled.

Request
curl -X POST https://pigeonatlas.com/v1/sequences/pricing-guide/enrollments \
  -H "Authorization: Bearer $PIGEONATLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane@example.com", "data": { "guide_title": "The pricing teardown" } }'
Response, 201
{
  "object": "enrollment",
  "sequence": "pricing-guide",
  "email": "jane@example.com",
  "status": "active",
  "stop_reason": null,
  "enrolled_at": "2026-10-15T09:30:03Z",
  "steps_sent": 0,
  "next_step": "welcome",
  "next_send_at": "2026-10-15T09:30:03Z",
  "last_error": null,
  "completed_at": null,
  "stopped_at": null,
  "created": true
}

Safe to repeat. A person goes through a sequence once. Enrolling them again answers 200 with "created": false and the enrollment as it is, active, finished or stopped, and changes nothing.

4. Stop when they buy

A sequence that keeps selling to a customer is the fastest way to an unsubscribe. When they buy, stop it with reason converted; for anything else, manual.

Request
curl -X DELETE https://pigeonatlas.com/v1/sequences/pricing-guide/enrollments/jane@example.com \
  -H "Authorization: Bearer $PIGEONATLAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "converted" }'

The answer is the enrollment, with "stopped": true, or false if it had already ended.

Merge variables

Values are escaped in HTML, so a name typed into your signup form is text, never markup. A misspelled variable is refused when you save the sequence rather than sent as a blank. A step without {{unsubscribe_url}} gets a small footer with the link, your brand's name and its mailing address. Every message also carries the one-click List-Unsubscribe header Gmail and Yahoo require.

Timing

What stops a sequence

If your account is out of allowance or your domain stops being verified, steps wait and are retried every 30 minutes; nobody is dropped for a reason that is not theirs.

Numbers

GET https://pigeonatlas.com/v1/sequences/:key returns the definition with each step's counts, and the Sequences page in the dashboard shows the same per brand. Opens are a floor: a mail app that blocks images reports none.

Response, abridged
{
  "object": "sequence",
  "key": "pricing-guide",
  "status": "active",
  "enrollments": { "active": 12, "completed": 30, "stopped": 2 },
  "steps": [
    { "key": "welcome", "delay_hours": 0, "subject": "Your pricing guide, {{name|there}}", "version": 2,
      "stats": { "sent": 44, "delivered": 43, "opened": 25, "clicked": 11,
                 "unsubscribed": 1, "bounced": 1, "complained": 0 } }
  ],
  "removed_steps": []
}

Errors

The same shape as everywhere else in the API, with a message that says what to change.

Status and nameMeans
402 plan_required Your plan does not include newsletters. Choose Growth or Scale.
403 restricted_api_key The key lacks contacts:write or sequences:write, or the sequence belongs to another brand.
403 invalid_from_address The sender is on a transactional domain, or on another brand's domain.
403 not_found / validation_error The sender's domain is not yours, or not verified yet.
404 not_found No such list, sequence, contact or enrollment.
404 contact_not_found Enrolling someone who is not a contact yet. Add them with consent first.
409 not_subscribed Enrolling someone not subscribed to the sequence's list, or who unsubscribed, bounced or complained. Nothing to retry.
409 conflict Changing the list of a sequence that has people in it, or two saves at once.
422 validation_error Something in the request is missing or malformed, such as consent or a merge variable.

Machine-readable

Every endpoint here is in the OpenAPI description. How newsletters work