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
- 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. - A list. Create it under Newsletters, Lists. Its page shows the slug your code uses, such as
acme-guides. - 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. - 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.
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"]
}'
{
"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
}
- The answer is 200 for a new and an existing contact;
createdsays which. statusissubscribed,unsubscribed,not_subscribed,bounced,complainedorsuppressed, for the list in the request.- Someone who unsubscribed, bounced or complained stays that way. The consent is recorded, and the person is not resubscribed: only they can undo their own unsubscribe.
- Tags are added and never removed, so two apps tagging one person do not erase each other.
GET https://pigeonatlas.com/v1/contacts/jane@example.comreturns the same shape, with the sequences the person is in now.
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.
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." }
]
}'
- Steps: 1 to 20, each with a
key,delay_hoursfrom enrollment, asubjectandhtml,textor both. Delays must increase from one step to the next. - Changing a step's copy never resends it. A step is known by its key; new words raise its
versionand reach only people who have not received it yet. A step you leave out stops being sent and keeps its numbers. - The list cannot change once people are enrolled. Use a new key for a sequence on another list.
statusmay beactiveorpaused; left out, it stays as it is. You can also pause from the dashboard.
3. Enroll them
Right after adding them. Only a contact subscribed to the sequence's list can be enrolled.
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" } }'
{
"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.
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
{{name}},{{first_name}},{{last_name}}and{{email}}from the contact.{{data.anything}}from thedatayou send when enrolling: flat, text or numbers, up to 50 keys.- A fallback after a bar:
{{name|there}}. {{unsubscribe_url}}, the person's own one-click link for this list.
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
- A step is due
delay_hoursafter enrollment. A step at 0 goes within about a minute. - Quiet hours are optional: from
starttoendin your time zone nothing is sent, and a step due then goes when they end. - If a step went out late (quiet hours, a pause, your allowance ran out), the next one keeps its spacing from it, so two steps never arrive minutes apart.
- Paused sequences send nothing; people enrolled meanwhile start when you resume.
- Steps you add later reach people still in the sequence, not people who finished it.
What stops a sequence
- Unsubscribing from the list, by the link or their mail app's button, stops every sequence on that list for them. Their receipts and password resets still arrive.
- A spam complaint or a hard bounce stops every sequence on your account for that address.
- Your app, with
convertedormanual. - The last step, which finishes it.
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.
{
"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 name | Means |
|---|---|
| 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