# Add A/B Test Variant
Source: https://docs.sequenzy.com/api-reference/ab-tests/add-variant
POST /api/v1/ab-tests/{abTestId}/variants
Add a variant to a draft campaign or sequence A/B test
Add a variant to a draft campaign or sequence A/B test. The next variant label (B, C, ...) is assigned automatically. A test can have at most 5 variants, and variants cannot be added after the test has started. Sequence variants receive their own editable email template; when the parent sequence is active, pass `confirmLiveChange: true` because the new variant immediately enters the live rotation.
## Request
A/B test ID.
Variant subject line.
Variant preview text.
Variant body blocks. Defaults to the campaign or sequence control email blocks when omitted.
Required as `true` when the A/B test belongs to an active sequence.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/variants" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Third subject idea"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"campaignId": "camp_abc123",
"status": "draft",
"variants": [
{ "id": "var_a", "label": "A", "subject": "A quick update" },
{ "id": "var_b", "label": "B", "subject": "Alternative subject line" },
{ "id": "var_c", "label": "C", "subject": "Third subject idea" }
]
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Cannot add variants after test has started"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Adding a variant to an A/B test in an active sequence requires confirmLiveChange=true"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Maximum of 5 variants allowed"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "A/B test not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create A/B test variant"
}
```
# Create A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/create
POST /api/v1/ab-tests
Create a campaign A/B test
Create a draft A/B test for a campaign in `draft` or `rejected` status, or convert a sequence email node into an A/B test step. Provide exactly one of `campaignId` or `automationNodeId`. Variant A (the control) is copied from the current email. A campaign or sequence node can have only one A/B test.
## Request
Campaign to attach the test to. Must be in `draft` or `rejected` status.
Mutually exclusive with `automationNodeId`.
Sequence `action_email` node to convert into an `action_ab_test` step.
Mutually exclusive with `campaignId`. Requires at least one `variants` entry.
Required as `true` when `automationNodeId` belongs to an active sequence. This
acknowledges that the live sequence graph will change immediately.
Test name. Defaults to `A/B Test for `.
Campaign-only share of the audience that receives test sends. Must be from 5
to 50. Defaults to 20. Sequence tests use `winnerThreshold` instead.
Campaign-only duration before a winner is selected. Must be from 15 to 1440
minutes. Defaults to 240.
Metric used to pick the winner: `open_rate` or `click_rate`. Campaigns default
to `open_rate`. For sequence tests, an explicit value overrides the `testType`
default.
Sequence variant strategy: `subject` or `content`. Subject defaults to
`open_rate` and content defaults to `click_rate` unless `winnerCriteria` is
explicit. Defaults to `content`. Sequence tests only.
Number of sequence recipients in the test sample, from 10 to 1000. Defaults to
100\. Sequence tests only. After the threshold, the variant with the highest
open or click rate wins, excluding bot opens and clicks. Recipients keep being
split across variants while no variant has engagement for `winnerCriteria` or
the leaders are tied. The leader is rechecked every 5 minutes. If tracking for
`winnerCriteria` is off, select the winner manually.
Extra variants beyond the control. Each item takes `subject` (required),
`previewText`, and `blocks`. Total variants cannot exceed 5. Variants without
`blocks` reuse the current email body. Optional for campaign tests; sequence
conversions require at least one entry.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "camp_abc123",
"testPercentage": 20,
"winnerCriteria": "open_rate",
"variants": [{ "subject": "Alternative subject line" }]
}'
```
For a sequence test, copy the effective values from the source test's
`settings` object:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"automationNodeId": "node_abc123",
"testType": "content",
"winnerCriteria": "open_rate",
"winnerThreshold": 150,
"variants": [{ "subject": "Alternative subject line" }]
}'
```
A sequence conversion moves that step's copy off the sequence node and onto the
test's variants. The response echoes this as `contentEditing`: from then on the
step's subject, preview text, and blocks are edited per variant with
[Update Variant](/api-reference/ab-tests/update-variant), not through the
sequence node endpoints, and a change meant for the whole step has to be applied
to every variant. The conversion response also returns the rewritten
`nodeConfig`.
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"kind": "campaign",
"campaignId": "camp_abc123",
"name": "A/B Test for April Launch",
"status": "draft",
"testPercentage": 20,
"testDurationMinutes": 240,
"winnerCriteria": "open_rate",
"settings": {
"testPercentage": 20,
"testDurationMinutes": 240,
"winnerCriteria": "open_rate"
},
"variants": [
{ "id": "var_a", "label": "A", "subject": "A quick update" },
{ "id": "var_b", "label": "B", "subject": "Alternative subject line" }
]
}
}
```
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_def456",
"kind": "sequence",
"automationNodeId": "node_abc123",
"status": "draft",
"testType": "content",
"winnerThreshold": 150,
"winnerCriteria": "open_rate",
"variants": [
{ "id": "var_a", "label": "A", "subject": "A quick update" },
{ "id": "var_b", "label": "B", "subject": "Alternative subject line" }
]
},
"nodeConfig": { "abTestId": "ab_def456", "variantCount": 2 },
"contentEditing": {
"readTool": "get_ab_test",
"writeTool": "update_ab_test_variant",
"requiredScopes": ["ab_tests:read", "ab_tests:write", "sequences:write"],
"note": "This step's copy lives on the A/B test variants..."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "testPercentage must be between 5 and 50"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Campaign structure can only be changed in draft or rejected status."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Maximum of 5 variants allowed"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
```json 409 theme={null}
{
"success": false,
"error": "A/B test already exists for this campaign"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create A/B test"
}
```
# Delete A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/delete
DELETE /api/v1/ab-tests/{abTestId}
Delete a campaign A/B test
Delete a campaign A/B test and all of its variants. Running tests (`testing` or `winner_selected`) cannot be deleted, and the linked campaign must be in `draft` or `rejected` status. This action cannot be undone.
## Request
A/B test ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "Cannot delete a running A/B test"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Only campaign A/B tests are supported by this endpoint."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Campaign structure can only be changed in draft or rejected status."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "A/B test not found"
}
```
# Delete A/B Test Variant
Source: https://docs.sequenzy.com/api-reference/ab-tests/delete-variant
DELETE /api/v1/ab-tests/{abTestId}/variants/{variantId}
Remove a variant from a draft campaign or sequence A/B test
Remove a variant from a draft campaign or sequence A/B test. Variant A is the control and cannot be deleted, and a test must keep at least 2 variants. Deleting a sequence variant also deletes its dedicated email template; when the parent sequence is active, pass `confirmLiveChange=true` because the deletion immediately changes the live rotation.
## Request
A/B test ID.
Variant ID to delete.
Required as `true` when the A/B test belongs to an active sequence.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/variants/var_c" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"campaignId": "camp_abc123",
"status": "draft",
"variants": [
{ "id": "var_a", "label": "A", "subject": "A quick update" },
{ "id": "var_b", "label": "B", "subject": "Alternative subject line" }
]
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Cannot remove variants after test has started"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Variant A is the control variant and cannot be deleted"
}
```
```json 400 theme={null}
{
"success": false,
"error": "A/B test must have at least 2 variants"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Deleting a variant from an A/B test in an active sequence requires confirmLiveChange=true"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Variant not found"
}
```
# Get A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/get
GET /api/v1/ab-tests/{abTestId}
Get A/B test details and variants
Get one A/B test, including its effective `settings`, all variants, and
localization sync status for each variant. Use `settings` when copying a test:
campaigns return percentage/duration settings, while sequences return
`testType`, `winnerThreshold`, and `winnerCriteria`.
Sequence records retain legacy `testPercentage: 100` and
`testDurationMinutes: 0` fields for backwards compatibility. Those sentinels do
not control sequence delivery.
## Request
A/B test ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"kind": "sequence",
"name": "Subject test",
"status": "draft",
"automationNodeId": "node_abc123",
"winnerCriteria": "open_rate",
"testType": "content",
"winnerThreshold": 150,
"settings": {
"testType": "content",
"winnerThreshold": 150,
"winnerCriteria": "open_rate"
},
"winningVariantId": null,
"variants": [
{
"id": "var_a",
"label": "A",
"emailId": "email_abc123",
"subject": "Welcome",
"previewText": null,
"blocks": [],
"localizations": []
}
]
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "A/B test not found"
}
```
## Campaign progress
Campaign detail includes `testStartedAt`, `testEndsAt`, `updatedAt`,
`pendingSampleUpdate` and `progress`. Compare variant subjects, preview text and
blocks to see what differs; `winnerCriteria` identifies how the winner is chosen.
Progress includes campaign status and pause reason, original `audienceSize`,
unique `committed` and `sent` recipient counts, `remaining` uncommitted recipients,
and `minimumTestPercentage`. Per-variant counts include committed, sent, pending
and failed recipients. Successful retries supersede earlier failures. Separate
manual test emails are excluded. A sent count means the provider accepted the
send, not inbox delivery. Legacy audience size and derived remaining/minimum
values can be null.
A pending sample request contains its ID, target `testPercentage`, `requestedAt`,
optional publication `startedAt`, and optional `error`. Null means no pending
request. Use [Update A/B Test](./update) to retry the same target or discard a
failed change. Progress is available even if engagement analytics is unavailable.
# List A/B Tests
Source: https://docs.sequenzy.com/api-reference/ab-tests/list
GET /api/v1/ab-tests
List A/B tests and their variants
List A/B tests for the authenticated company. You can filter to tests attached
to a sequence. Each result includes `kind` and an effective `settings` object;
use `settings` when copying a test.
## Request
Optional sequence ID. When provided, only A/B tests attached to that sequence
are returned.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/ab-tests?sequenceId=seq_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"abTests": [
{
"id": "ab_abc123",
"kind": "sequence",
"name": "Subject test",
"status": "draft",
"automationNodeId": "node_abc123",
"testType": "content",
"winnerThreshold": 150,
"winnerCriteria": "open_rate",
"settings": {
"testType": "content",
"winnerThreshold": 150,
"winnerCriteria": "open_rate"
},
"variants": [
{
"id": "var_a",
"label": "A",
"subject": "Welcome",
"previewText": null,
"blocks": []
}
]
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Restart A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/restart
POST /api/v1/ab-tests/{abTestId}/restart
Run another sequence A/B test
Start a new draft A/B test for the same sequence node after a winner has been selected. The previous test and metrics stay intact, and the new test becomes active after its generated variants are ready.
## Request
A/B test ID to restart.
Variant ID to use as the new control email. Defaults to the selected winner.
Test type: `subject` or `content`. Defaults to `subject`.
Subscribers before selecting a winner. Must be from 10 to 1000.
Total variants including the control. Must be from 2 to 4.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/restart" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sourceVariantId": "var_b", "testType": "content", "variantCount": 3}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"previousAbTestId": "ab_abc123",
"sourceVariantId": "var_b",
"abTest": {
"id": "ab_new123",
"status": "draft",
"variants": [{ "id": "var_new_a", "label": "A" }]
},
"nodeConfig": {
"abTestId": "ab_abc123",
"winnerId": "var_winner",
"pendingAbTestId": "ab_new123",
"pendingExpectedVariantCount": 3
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "A winner must be selected before running another A/B test."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Selected control email not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create A/B test"
}
```
# Resume A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/resume
POST /api/v1/ab-tests/{abTestId}/resume
Keep testing a sequence A/B test after a winner was picked
Resume a sequence A/B test that already has a winner, for example one picked
automatically before you were sure. The winner is cleared and contacts who
reach the step are split across the variants again. Results collected so far
are kept, and contacts who already got a variant keep it.
Automatic selection is turned off for the test (`autoSelectWinner: false`), so
it keeps splitting until you choose a winner with
[Select A/B Test Winner](./select-winner). You can turn it back on with
`autoSelectWinner: true` on [Update A/B Test](./update). The test returns to
`testing`, so its variants stay fixed until a winner is selected. Repeating the
request is safe.
## Request
Sequence A/B test ID.
Required as `true` when the sequence is active, because contacts reaching the
step are split again from then on. Not needed to repeat a resume that already
applied.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/resume" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"confirmLiveChange":true}'
```
The API key needs `ab_tests:write` and `sequences:write`.
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"kind": "sequence",
"status": "testing",
"winningVariantId": null,
"autoSelectWinner": false
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Resuming an A/B test in an active sequence requires confirmLiveChange=true"
}
```
400 also covers campaign tests, completed or cancelled tests, a test that is no
longer attached to its step, and a test with fewer than two variants; 401/403
cover authentication, scope and role restrictions; 404 means the test is
unavailable.
# Select A/B Test Winner
Source: https://docs.sequenzy.com/api-reference/ab-tests/select-winner
POST /api/v1/ab-tests/{abTestId}/select-winner
Select a campaign or sequence A/B test winner
Select a variant as the winner of a campaign or sequence A/B test.
* **Campaign tests** must be in the `testing` phase. Selection immediately
queues winner delivery for the remaining audience. Repeating the same
selection safely repairs an interrupted queue operation; selecting a
different winner returns a conflict.
* **Sequence tests** can be in any phase except `completed` or `cancelled`.
You can also change a winner that was already selected, including one picked
automatically after the test reached its `winnerThreshold` and one variant
led on the winning metric. The test needs at least two variants. Everyone who
reaches the step from then on gets the new winner. Contacts who already got
a variant keep it. Only the test currently attached to its sequence step can
be changed. Repeating the same selection returns 200 without changing
`winnerSelectedAt`. The response `abTest` has the same shape as
[Get A/B Test](./get).
## Request
Campaign or sequence A/B test ID.
Variant ID to select as the winner.
Sequence tests only. Required as `true` when the sequence is active, because
the new winner goes to everyone who reaches the step from then on. Not needed
to repeat the current winner. Campaign tests ignore it.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/select-winner" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"variantId":"var_b"}'
```
The API key needs `ab_tests:write`. Campaign tests also need `campaigns:send`;
sequence tests also need `sequences:write`.
## Responses
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"status": "winner_selected",
"winningVariantId": "var_b"
}
}
```
```json 409 theme={null}
{
"success": false,
"error": "Another winner was selected at the same time"
}
```
For campaign tests, a pending audience change returns 409. Wait for it to
apply, or discard a failed change before selecting your winner. You can record
a winner while paused; delivery waits until you resume the campaign. If queue
publication returns 500 after selection, GET the test and repeat the same
winner ID. That retry repairs delivery without changing the winner or resending
committed recipients.
400 covers an invalid phase, a missing or inactive campaign, a sequence test
that is no longer attached to its step or has only one variant, or a missing
`confirmLiveChange` for an active sequence; 401/403 cover authentication, scope and
role restrictions; 404 means the test or the requested variant is unavailable.
# Get A/B Test Stats
Source: https://docs.sequenzy.com/api-reference/ab-tests/stats
GET /api/v1/ab-tests/{abTestId}/stats
Get aggregate and per-variant A/B test stats
Get aggregate A/B test stats and per-variant stats for the authenticated company. Open and click metrics exclude detected email-security scanners and tracked brand assets by default.
The response also reports whether the difference in the test's winner metric (open rate or click rate) is statistically significant, so you can tell a real winner from noise.
## Request
A/B test ID.
Optional period: `1h`, `24h`, `7d`, `30d`, or `90d`.
Custom range start as ISO 8601. Requires `end`.
Custom range end as ISO 8601. Requires `start`.
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in A/B test metrics.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/stats?period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Statistical significance of the test's winner metric. Before a winner is
selected it uses the same time range as the stats. Once a winner is
selected, it compares only the emails sent before the winner was chosen,
with all of their later opens and clicks, and ignores `period`, `start` and
`end`: the winner's later sends to the rest of the audience are counted
under its variant and would skew the comparison. For a sequence test whose
winner you later changed or resumed, the comparison also includes the
earlier winner's sends. The leading variant is compared with every
other variant using a two-sided two-proportion z-test, corrected for the
number of comparisons. Rates use delivered emails, or sends when no
deliveries were recorded.
`open_rate` or `click_rate`, from the test's winner criteria.
`insufficient_data` when a variant has fewer than 30 recipients or too
few opens/clicks for a reliable result, `not_significant` when the
difference could still be chance, or `significant` when confidence
reaches `confidenceLevel`.
Confidence required for `significant`: `0.95`.
Variant with the highest rate. `null` when the top variants are tied.
Label of the leading variant, such as `B`.
One minus `pValue`, from 0 to 1. At or above `confidenceLevel`, the
leader's advantage over every other variant is unlikely to be chance.
Very strong results can round to exactly `1`; treat that as above 99.9%.
`null` while `status` is `insufficient_data`.
Corrected two-sided p-value of the weakest leader comparison. `null`
while `status` is `insufficient_data`.
How much higher the leader's rate is than the closest variant's (`0.3`
means 30% higher). `null` without a leader or when the closest variant
has no opens or clicks.
Significance is informational. Automatic winner selection still picks the
variant with the highest rate when the test period ends.
## Responses
```json 200 theme={null}
{
"success": true,
"abTestId": "ab_abc123",
"period": "30d",
"stats": {
"sent": 200,
"delivered": 198,
"opened": 92,
"clicked": 18,
"openRate": 46.46,
"clickRate": 9.09
},
"variants": [
{
"id": "var_a",
"label": "A",
"subject": "Welcome",
"isWinner": false,
"stats": {
"sent": 100,
"delivered": 99,
"opened": 42,
"clicked": 7,
"openRate": 42.42,
"clickRate": 7.07
}
},
{
"id": "var_b",
"label": "B",
"subject": "Welcome aboard",
"isWinner": false,
"stats": {
"sent": 100,
"delivered": 99,
"opened": 50,
"clicked": 11,
"openRate": 50.51,
"clickRate": 11.11
}
}
],
"significance": {
"metric": "open_rate",
"status": "not_significant",
"confidenceLevel": 0.95,
"leaderVariantId": "var_b",
"leaderVariantLabel": "B",
"confidence": 0.7457,
"pValue": 0.2543,
"relativeLift": 0.1905
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid period. Must be one of: 1h, 24h, 7d, 30d, 90d"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "A/B test not found"
}
```
# Update A/B Test
Source: https://docs.sequenzy.com/api-reference/ab-tests/update
PATCH /api/v1/ab-tests/{abTestId}
Update campaign or sequence A/B test settings
Update an A/B test's effective settings. Campaign and sequence tests use
different selection models:
* Campaign tests use `testPercentage`, `testDurationMinutes`, and
`winnerCriteria`.
* Sequence tests use `testType`, `winnerThreshold`, `winnerCriteria`, and
`autoSelectWinner`.
Sequence settings that affect an active test or a test with recorded activity
require `confirmLiveChange: true`.
Campaign tests support settings changes while `draft` or `testing`. During testing,
your campaign must be sending or paused. Duration is the total time from
`testStartedAt`, not extra time from now. A deadline in the past requests winner
selection immediately; paused campaigns stay paused.
Live audience changes use an integer percentage of the original full audience.
Already committed recipients keep their variant, including pending and ambiguous
send attempts. The worker waits for published sample jobs to settle and validates
again before applying your request. It can reject a shrink if sends increased
meanwhile. Legacy tests without a verifiable sample plan cannot resize.
## Request
A/B test ID.
Updated test name.
Campaign-only share of the audience used for testing, from 5 to 50.
Campaign-only duration before winner selection, from 15 to 1440 minutes.
Winner metric for either test kind: `open_rate` or `click_rate`. You cannot
change this metric after campaign testing starts.
Sequence-only variant strategy: `subject` or `content`. When `winnerCriteria`
is omitted, subject defaults to `open_rate` and content defaults to
`click_rate`.
Sequence-only number of recipients before selecting a winner, from 10 to 1000.
After the threshold, the variant with the highest open or click rate wins,
excluding bot opens and clicks. Recipients keep being split across variants
while no variant has engagement for `winnerCriteria` or the leaders are tied.
The leader is rechecked every 5 minutes. If tracking for `winnerCriteria` is
off, select the winner manually.
Sequence-only. `true` (the default) picks the leading variant once
`winnerThreshold` is reached. `false` keeps splitting contacts until you
choose a winner with [Select A/B Test Winner](./select-winner). [Resume A/B
Test](./resume) sets it to `false`. Turning it back on can pick a winner on
the next send when the threshold is already reached.
Optional campaign revision from your last GET, as an ISO timestamp. A stale
revision returns 409. Omit it for existing unconditional update behavior.
Discard a failed campaign sample request. Send this without `testPercentage`.
It clears pending intent, not emails or recipients already committed.
Required as `true` when changing selection settings for an active sequence
test or one with recorded activity.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"testType": "content",
"winnerCriteria": "open_rate",
"winnerThreshold": 150,
"confirmLiveChange": true
}'
```
## Response
The response includes `kind` and an effective `settings` object. Use
`settings` when copying a test. The legacy `testPercentage: 100` and
`testDurationMinutes: 0` fields on sequence tests are retained for backwards
compatibility and are not sequence runtime settings.
```json 200 theme={null}
{
"success": true,
"abTest": {
"id": "ab_abc123",
"kind": "sequence",
"automationNodeId": "node_abc123",
"winnerCriteria": "open_rate",
"testType": "content",
"winnerThreshold": 150,
"settings": {
"testType": "content",
"winnerThreshold": 150,
"winnerCriteria": "open_rate"
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Sequence A/B tests use testType and winnerThreshold; testPercentage and testDurationMinutes apply only to campaign A/B tests"
}
```
```json 404 theme={null}
{
"success": false,
"error": "A/B test not found"
}
```
## Live request recovery
A successful update returns current `progress` and `pendingSampleUpdate`.
`testPercentage` stays at the last applied value until the worker publishes the
new sample. Poll [Get A/B Test](./get) until `pendingSampleUpdate` becomes null.
Omitted settings remain unchanged. Request settings do not accept null.
Repeat the same `testPercentage` to repair an interrupted enqueue or worker run;
the request keeps its ID. A different pending target returns 409. When `error`
is present you can retry or send `cancelSampleUpdate: true`. Active requests
cannot be discarded. A retry uses deterministic jobs and preserves committed
recipient assignments. Winner selection waits while a sample request is pending.
400 covers invalid limits, unavailable start/audience information, an immutable
winning metric, or a shrink below committed sends. 401/403 cover authentication,
scopes or role restrictions; 404 covers missing or inaccessible tests. A 500 may
occur after saving settings if queue publication fails: GET the current state
and retry the same settings. `ab_tests:write` remains sufficient for campaign
settings updates; finishing early requires `campaigns:send` as well.
When reducing a campaign sample, only queue jobs whose sends have never started and that have no committed recipients can be withdrawn. Active jobs and pending transport retries finish first, so the minimum can rise before the change applies. Retrying preserves the request; discarding a failed change keeps work already performed, including withdrawn sample jobs. Recipients without committed sends remain eligible for the eventual winner.
# Update A/B Test Variant
Source: https://docs.sequenzy.com/api-reference/ab-tests/update-variant
PATCH /api/v1/ab-tests/{abTestId}/variants/{variantId}
Update an A/B test variant
Update an A/B test variant's subject, preview text, or body content. You can use raw HTML or Sequenzy block JSON for the body. Campaign variants remain editable only while the test is in draft. Sequence variants can be edited later with explicit confirmation because the change affects future sends and can make results combine different content versions.
## Request
A/B test ID.
Variant ID.
Updated subject line.
Updated preview text. Send `null` to clear it.
Replacement HTML body. Use this or `blocks`, not both.
Replacement Sequenzy email blocks. Use this or `html`, not both. Put block
styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
Required as `true` when the sequence is active, the test is no longer a draft,
or the test has recorded activity. Earlier sends are unchanged, so the
combined A/B test results may no longer be accurate.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/ab-tests/ab_abc123/variants/var_b" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "A better subject", "confirmLiveChange": true}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"variant": {
"id": "var_b",
"label": "B",
"emailId": "email_def456",
"subject": "A better subject",
"previewText": null,
"blocks": [],
"localizations": []
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Previous sends cannot be changed. Editing this variant changes future sends that use it, so the A/B test results may combine different versions and may no longer be accurate. Set confirmLiveChange=true to continue."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Variant not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to update variant"
}
```
# Get API Key Context
Source: https://docs.sequenzy.com/api-reference/account/get
GET /api/v1/account
Get companies and the authenticated API key's permission summary
Get the current API context. User-scoped keys return the companies you can access and the selected company; company-scoped keys return that single company. The response also reports the authenticated key's non-secret identity, its effective permission preset, its scopes, and a direct API Keys management URL. Personal keys link to Account API Keys, while company keys link to the selected workspace's API Keys settings. Use `activeKey` to confirm which credential the client loaded before editing it in place. Only `scopes: null` is forward-compatible full access; an explicit array containing every current scope remains a fixed custom selection. This is permission metadata only and does not bypass resource scopes.
This endpoint requires `account:read`. If a custom key does not include that scope, open the [Sequenzy dashboard](https://sequenzy.com/dashboard) directly and edit the active key from Account API Keys or the selected workspace's API Keys settings.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/account" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"user": {
"id": "user_abc123"
},
"companies": [
{
"id": "company_abc123",
"name": "Acme Inc",
"role": "owner"
}
],
"currentCompanyId": "company_abc123",
"apiKeyPermissions": {
"activeKey": {
"id": "key_abc123",
"name": "Cursor custom access",
"prefix": "seq_live_A",
"type": "company"
},
"preset": "custom",
"fullAccess": false,
"selectedScopeCount": 2,
"currentScopeCount": 63,
"description": "2 of 63 current permissions are enabled.",
"scopes": ["account:read", "subscribers:read"],
"canDiscoverMarketingWork": false,
"missingMarketingReadScopes": [
"campaigns:read",
"sequences:read",
"landing_pages:read"
],
"canSendLive": false,
"liveDeliveryBlockedByRole": false,
"missingLiveDeliveryScopes": [
"automations:trigger",
"campaigns:send",
"sequences:activate",
"sequences:enroll",
"transactional:send",
"conversations:write",
"team:manage"
],
"manageUrl": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys"
}
}
```
`canSendLive` is `false` when the key holds no scope that delivers to a real
person, and `missingLiveDeliveryScopes` lists the delivery paths it lacks.
Drafting and delivery are separate permissions: a key can hold
`transactional:write` to manage transactional templates while lacking
`transactional:send` to deliver one. Check these fields before composing
content you intend to send.
`liveDeliveryBlockedByRole` is `true` when the authenticated user's role in the
selected workspace blocks sending regardless of key scopes - a personal key held
by a `viewer` is read-only, so widening the key's permissions will not enable
delivery.
Account-scoped keys return `role` for each company. `owner` and `admin` can use
write permissions granted to the key; `marketer` can use marketing permissions
but not transactional, settings, integration, webhook, team, or API-key ones
(the blocked scopes are listed in `apiKeyPermissions.roleRestrictedScopes`);
`viewer` is always limited to read operations.
The selected company changes API context, not billing ownership. Companies with
the same owner share the owner's core email plan and monthly usage pool; a
company you joined uses its own owner's pool. See
[Workspaces, Billing, and Shared Limits](/concepts/workspaces-and-billing) for
the full billing and sender-health scope rules.
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Get Integration Guide
Source: https://docs.sequenzy.com/api-reference/account/integration-guide
POST /api/v1/integration-guide
Get a framework-specific API integration example
Get a short code example and implementation tip for a framework and use case.
## Request
Framework or deployment description. Defaults to `nextjs`. For
`subscribe_form`, values containing `static`, `astro`, `hugo`, `jekyll`,
`cloudflare pages`, `netlify`, or `github pages` return the client-safe Saved
Forms workflow instead of secret-key subscriber API code.
Use case key: `transactional`, `subscribe_form`, `event_tracking`, or
`ecommerce`. Defaults to `transactional`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/integration-guide" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"framework": "nextjs", "use_case": "transactional"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"framework": "nextjs",
"use_case": "transactional",
"code": "await fetch('https://api.sequenzy.com/api/v1/transactional/send', ...)",
"tip": "For transactional emails, make sure to set up a verified sending domain in the dashboard for best deliverability."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
For a static-site signup form, the response points you to `list_forms`,
`create_form`, and `get_form_embed`, then returns a native form action shaped
like `https://api.sequenzy.com/api/v1/forms/{formId}`. That deployed action does
not require an API key because its audience and success behavior are controlled
by the saved form.
# Delete Account
Source: https://docs.sequenzy.com/api-reference/accounts/delete
DELETE /api/v1/accounts/{externalId}
Delete an account and its memberships
Deletes the account and its memberships. Contacts are kept; their `account.*` attributes are cleared (or replaced by another account they belong to).
## Request
Your organization ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/accounts/org_123" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{ "success": true, "deleted": true, "membersCleared": 2 }
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
# List Account Events
Source: https://docs.sequenzy.com/api-reference/accounts/events-list
GET /api/v1/accounts/{externalId}/events
Recent events on an account's timeline
Returns recent account events, newest first.
## Request
Your organization ID.
Number of events, up to 500.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/accounts/org_123/events?limit=20" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"account": { "id": "acc_abc123", "externalId": "org_123" },
"events": [
{ "id": "evt_1", "name": "trial_ending", "eventTime": "2026-09-04T09:00:00.000Z", "properties": { "daysLeft": 3 }, "recipientCount": 1 }
]
}
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
# Trigger Account Event
Source: https://docs.sequenzy.com/api-reference/accounts/events-trigger
POST /api/v1/accounts/{externalId}/events
Record an event on an account and deliver it to its members
Records the event on the account timeline and delivers it to the chosen recipients as a normal contact event, so sync rules, sequence triggers and matching-field enrollment run per person. Each delivery carries `event.account.externalId`, `event.account.name`, `event.account.role` and the account attributes.
## Request
Your organization ID.
Event name, for example `trial_ending`, `plan_changed`, `seat_limit_reached`.
Event properties, available as `event.` in sequences.
`owners` (falls back to admins when the account has no owner), `admins`
(owners and admins), `all` (every member) or `none` (timeline only).
Idempotency key scoped to this account. Accounts can reuse an ID even when
they share members. Retries reuse the original recipients, properties and
event time, even if account membership changes. Completed deliveries are
skipped; failed deliveries resume without duplicating contact events or
sequence enrollments. Recipients deleted since the event was recorded are
reported with `skipped: "contact_deleted"` and are not retried. The response
carries `duplicate: true`.
ISO 8601 timestamp of when the event happened. Defaults to now.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/accounts/org_123/events" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "trial_ending", "properties": { "daysLeft": 3 }, "recipients": "owners", "eventId": "trial-org_123-2026-09-04" }'
```
## Responses
```json 200 theme={null}
{
"success": true,
"account": { "id": "acc_abc123", "externalId": "org_123", "name": "Acme", "memberCount": 3 },
"event": { "id": "trial-org_123-2026-09-04", "name": "trial_ending", "time": "2026-09-04T09:00:00.000Z" },
"recipients": "owners",
"recipientCount": 1,
"truncated": false,
"duplicate": false,
"deliveries": [
{ "subscriberId": "sub_1", "email": "jane@acme.com", "role": "owner", "success": true, "automationsTriggered": ["auto_trial"] }
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Account event `recipients` must be one of owners, admins, all, none."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Delivered to 2 of 3 recipients; 1 failed. Retry with the same eventId to re-attempt only the failed recipients.",
"recipientCount": 3,
"deliveries": [
{
"subscriberId": "sub_2",
"email": "bob@acme.com",
"role": "owner",
"success": false,
"automationsTriggered": [],
"error": "..."
}
]
}
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
# Get Account by External ID
Source: https://docs.sequenzy.com/api-reference/accounts/get
GET /api/v1/accounts/{externalId}
Get an account with its members
Returns the account and up to 100 members, owners first. URL-encode external IDs that contain slashes.
## Request
Your organization ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/accounts/org_123" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"account": {
"id": "acc_abc123",
"externalId": "org_123",
"name": "Acme",
"domain": "acme.com",
"attributes": { "plan": "pro", "seats": 5 },
"memberCount": 2,
"lastEventAt": "2026-09-04T09:00:00.000Z",
"createdAt": "2026-08-01T10:00:00.000Z",
"updatedAt": "2026-09-04T09:00:00.000Z"
},
"members": [
{ "id": "mem_1", "accountId": "acc_abc123", "subscriberId": "sub_1", "role": "owner", "email": "jane@acme.com", "externalId": "user_1", "firstName": "Jane", "lastName": null, "status": "active", "createdAt": "2026-08-01T10:00:00.000Z" },
{ "id": "mem_2", "accountId": "acc_abc123", "subscriberId": "sub_2", "role": "member", "email": "bob@acme.com", "externalId": null, "firstName": null, "lastName": null, "status": "active", "createdAt": "2026-08-02T10:00:00.000Z" }
]
}
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
# List Accounts
Source: https://docs.sequenzy.com/api-reference/accounts/list
GET /api/v1/accounts
List the accounts (organizations) in your workspace
Lists accounts with search, paging and sorting. Accounts are the companies, workspaces or teams your contacts belong to. See [Accounts](/concepts/accounts).
## Request
Matches name, external ID and domain.
Page number.
Results per page, maximum 100.
One of `updatedAt`, `createdAt`, `name`, `memberCount`, `lastEventAt`.
`asc` or `desc`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/accounts?search=acme&sort=memberCount&order=desc" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"accounts": [
{
"id": "acc_abc123",
"externalId": "org_123",
"name": "Acme",
"domain": "acme.com",
"attributes": { "plan": "pro", "seats": 5 },
"memberCount": 6,
"lastEventAt": "2026-09-04T09:00:00.000Z",
"createdAt": "2026-08-01T10:00:00.000Z",
"updatedAt": "2026-09-04T09:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 25,
"hasMore": false
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
# Account Members
Source: https://docs.sequenzy.com/api-reference/accounts/members
POST /api/v1/accounts/{externalId}/members
List, add and remove the contacts that belong to an account
Members are contacts with a role: `owner`, `admin` or `member`. Account events pick recipients by role, and `{{account.role}}` is available in emails.
* `GET /api/v1/accounts/{externalId}/members` lists members (owners first, then admins, then members)
* `POST /api/v1/accounts/{externalId}/members` adds or updates a member
* `DELETE /api/v1/accounts/{externalId}/members` removes a member
## Request
Your organization ID.
`GET` only. Maximum members to return (up to 5,000).
Contact email. On `POST`, a new email creates the contact.
Contact external ID, as an alternative to `email`.
`POST` only. `owner`, `admin` or `member`. Re-adding an existing member with a
role updates it; without a role keeps the current one.
`POST` only. Set when creating the contact.
`POST` only. Set when creating the contact.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/accounts/org_123/members" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "jane@acme.com", "role": "owner" }'
curl -X DELETE "https://api.sequenzy.com/api/v1/accounts/org_123/members" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "bob@acme.com" }'
```
## Responses
```json 201 theme={null}
{
"success": true,
"account": { "id": "acc_abc123", "externalId": "org_123", "memberCount": 3 },
"member": { "subscriberId": "sub_1", "email": "jane@acme.com", "externalId": null, "role": "owner", "created": true, "subscriberCreated": false }
}
```
```json 200 theme={null}
{
"success": true,
"account": { "id": "acc_abc123", "externalId": "org_123" },
"members": [
{ "subscriberId": "sub_1", "email": "jane@acme.com", "role": "owner" }
]
}
```
```json 200 theme={null}
{
"success": true,
"account": { "id": "acc_abc123", "externalId": "org_123", "memberCount": 2 },
"removed": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide `email` to create a contact that does not exist yet."
}
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
# Create Accounts From an Organization ID
Source: https://docs.sequenzy.com/api-reference/accounts/organization-ids-create
POST /api/v1/account-suggestions/organization-ids
Create one account per organization ID and add its contacts as members
Starts a background job that creates one account per distinct value of `propertyKey` and adds each contact seen with that value as a `member`. [Preview](/api-reference/accounts/organization-ids-preview) the result first, then check progress with [Get Organization ID Job](/api-reference/accounts/organization-ids-job).
* Accounts are named from `nameKey` when you pass one, otherwise after the work email domain most of their contacts share. That domain is also saved on the account.
* Existing accounts keep their names, domains and member roles.
* No emails are sent, no sync rules run and segment-entered sequences are not triggered.
* For `event`, events from the last 180 days are used.
The first run turns Accounts on for the workspace, with the same `409` rule as [creating an account](/api-reference/accounts/upsert). Only one run per key runs at a time. While one is queued or running, this returns it with `alreadyRunning: true` instead of starting another. That run keeps its own settings, shown in `job.settings`; start again after it finishes to use a different `source` or `nameKey`. Accounts stays on even if the job later fails. Running it again later is safe: it updates the same accounts and memberships.
Requires the `subscribers:write` scope.
## Request
Event property or contact attribute holding your organization ID. Trimmed.
`event` for an event property, `attribute` for a contact attribute.
Property or attribute holding the organization name.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/account-suggestions/organization-ids" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"propertyKey": "organization_id"}'
```
## Response
Returns `202 Accepted`.
Pass to [Get Organization ID
Job](/api-reference/accounts/organization-ids-job).
`true` when a run for this key was already queued or running, so no new run
started.
The job's current status, as returned by [Get Organization ID
Job](/api-reference/accounts/organization-ids-job).
```json 202 theme={null}
{
"success": true,
"jobId": "backfill-accounts:comp_123:organization_id",
"alreadyRunning": false,
"job": {
"jobId": "backfill-accounts:comp_123:organization_id",
"state": "queued",
"settings": {
"source": "event",
"propertyKey": "organization_id",
"nameKey": null
},
"result": null,
"error": null
}
}
```
```json 400 theme={null}
{ "success": false, "error": "`propertyKey` cannot be blank." }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 409 theme={null}
{
"success": false,
"error": "Contacts in this workspace already use the custom attribute `account.plan`. Accounts reserve the `account` attribute name for organization data, so turning Accounts on would stop those contact attributes from updating in segments. Rename them (for example to `company.plan`), then try again."
}
```
```json 422 theme={null}
{ "success": false, "error": "Expected property 'propertyKey' to be string" }
```
# Detect Organization IDs
Source: https://docs.sequenzy.com/api-reference/accounts/organization-ids-detect
GET /api/v1/account-suggestions/organization-ids
Find the organization ID your app already sends in events or contact attributes
If your app already sends an organization ID, such as `workspaceId` on events or `account_id` on contacts, you can create accounts from it without writing code. This endpoint finds those keys. [Preview](/api-reference/accounts/organization-ids-preview) a key to see the accounts it would create, then [create them](/api-reference/accounts/organization-ids-create). See [Accounts](/concepts/accounts).
It looks at event properties from the last 180 days and at contact attributes whose names look like an organization ID, such as `workspaceId`, `organization_id`, `team_id`, `tenantId` or `account_id`. A key needs at least two distinct values. Nothing is created.
Requires the `subscribers:read` scope.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/account-suggestions/organization-ids" \
-H "Authorization: Bearer API_KEY"
```
## Response
Detected keys, most useful first. Empty when none are found.
`event` for an event property, `attribute` for a contact attribute.
The property or attribute holding the organization ID.
A sibling property holding the organization name, such as
`workspaceName`, when one is found.
Events carrying the key for `event`, contacts with the attribute for
`attribute`.
Distinct organizations, which is how many accounts creating would make.
Up to 8 event names carrying the key. Empty for attributes.
```json 200 theme={null}
{
"success": true,
"candidates": [
{
"source": "event",
"propertyKey": "organization_id",
"nameKey": null,
"eventCount": 2518,
"accountCount": 843,
"eventNames": ["signed_in", "project_created"]
}
]
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
# Get Organization ID Job
Source: https://docs.sequenzy.com/api-reference/accounts/organization-ids-job
GET /api/v1/account-suggestions/organization-ids/jobs/{jobId}
Check a run that creates accounts from an organization ID
Reports a job started by [Create Accounts From an Organization ID](/api-reference/accounts/organization-ids-create). Poll every few seconds until `state` is `completed` or `failed`.
Finished jobs are kept for a limited time. After that, this returns `404`, and the accounts it created stay.
Requires the `subscribers:read` scope.
## Request
The `jobId` returned when the job started, URL-encoded.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/account-suggestions/organization-ids/jobs/backfill-accounts%3Acomp_123%3Aorganization_id" \
-H "Authorization: Bearer API_KEY"
```
## Response
`queued`, `running`, `completed` or `failed`.
What the run reads: `source`, `propertyKey` and `nameKey`.
Present when `completed`: `accountsCreated`, `accountsUpdated` (existing
accounts that got a name or domain), `membershipsCreated`,
`membershipsUpdated`, `skipped` (contacts that no longer exist, or invalid
IDs) and `truncated` (history was larger than one run covers; start the
job again to continue).
Present when `failed`.
```json 200 theme={null}
{
"success": true,
"job": {
"jobId": "backfill-accounts:comp_123:organization_id",
"state": "completed",
"settings": {
"source": "event",
"propertyKey": "organization_id",
"nameKey": null
},
"result": {
"accountsCreated": 843,
"accountsUpdated": 0,
"membershipsCreated": 2104,
"membershipsUpdated": 0,
"skipped": 3,
"truncated": false
},
"error": null
}
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 404 theme={null}
{ "success": false, "error": "Job not found" }
```
# Preview Accounts From an Organization ID
Source: https://docs.sequenzy.com/api-reference/accounts/organization-ids-preview
GET /api/v1/account-suggestions/organization-ids/preview
See the accounts an organization ID would create before creating them
Shows the biggest organizations that [creating accounts](/api-reference/accounts/organization-ids-create) from `propertyKey` would make: the name each account would get, how many contacts would join it, and a few of those contacts. Nothing is created.
Each account is named from `nameKey` when you pass one. Otherwise it is named after the work email domain most of its contacts share, so `organization_id: zplgap...` with contacts at `leadpages.com` becomes **Leadpages**. Personal and disposable providers, such as `gmail.com`, and your own sending domains are ignored. A domain needs most of the organization's contacts, so one consultant among personal emails does not name it. When no name can be found, `name` is `null` and the account shows its ID. Accounts that already exist keep their current name and domain, and `contactCount` can include contacts that were deleted since, which are skipped when creating.
Requires the `subscribers:read` scope.
## Request
Event property or contact attribute holding your organization ID, from [Detect
Organization IDs](/api-reference/accounts/organization-ids-detect).
`event` for an event property, `attribute` for a contact attribute.
Property or attribute holding the organization name.
Organizations to return, biggest first. Values outside 1 to 50 are clamped,
and non-numeric values use the default.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/account-suggestions/organization-ids/preview?propertyKey=organization_id" \
-H "Authorization: Bearer API_KEY"
```
## Response
The biggest organizations first.
The organization ID, which becomes the account's external ID.
The name the account gets, or `null` when none can be found.
The work email domain most of its contacts share.
Contacts that would become members.
Up to 3 of those contacts, those at `domain` first.
```json 200 theme={null}
{
"success": true,
"organizations": [
{
"externalId": "zplgaplihqncouqlmqf8os3e",
"name": "Leadpages",
"domain": "leadpages.com",
"contactCount": 14,
"sampleEmails": [
"henny@leadpages.com",
"nicola@leadpages.com",
"corah@leadpages.com"
]
}
]
}
```
```json 400 theme={null}
{ "success": false, "error": "`propertyKey` cannot be blank." }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 422 theme={null}
{ "success": false, "error": "Expected property 'propertyKey' to be string" }
```
# Accept Account Suggestions
Source: https://docs.sequenzy.com/api-reference/accounts/suggestions-accept
POST /api/v1/account-suggestions
Create accounts from suggested email domains
Creates an account for each domain you confirm from [List Account Suggestions](/api-reference/accounts/suggestions-list). The account's external ID and domain are the domain, and its name is derived from it (`acme-corp.com` becomes `Acme Corp`). Every contact at the domain that belongs to no account is added as a `member`.
* When one account already uses the domain as its domain or external ID, the contacts are added to that account instead.
* Contacts already in another account stay where they are. If no account uses the domain but some of its contacts already belong to one, the domain is skipped so the organization is not duplicated.
* Existing roles, names and domains are kept. No sync rules run, and sequences that start when a contact enters a segment are not triggered.
* If a call fails partway, retrying it with the same domains finishes the work.
* Retries are safe: a repeated call adds nothing new and returns `updated`.
Accepting the first suggestion turns Accounts on for the workspace, with the same `409` rule as [creating an account](/api-reference/accounts/upsert). Requires the `subscribers:write` scope.
## Request
1 to 25 domains, such as `acme.com`. Domains are lowercased and duplicates are
ignored. A personal or disposable provider, one of your own sending domains,
or anything that is not a hostname rejects the whole request with `400` before
anything is written.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/account-suggestions" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["acme.com", "globex.io"]}'
```
## Response
Accounts created by this call.
Memberships created by this call.
One entry per domain, in request order.
The normalized domain.
`created` (new account), `updated` (existing account) or `skipped`.
For `skipped`: `no_contacts` when no contact at the domain is free to
add, `multiple_accounts` when several accounts already use the domain, or
`already_in_account` when some of the domain's contacts already belong to
an account that does not use the domain. Otherwise `null`.
The account, as returned by [Get Account](/api-reference/accounts/get).
`null` when skipped.
Eligible contacts at the domain: in no account, or already in this one.
New memberships for this domain.
`true` when contacts were left out because an account holds at most 5,000
members, the limit for account attributes on contacts.
```json 200 theme={null}
{
"success": true,
"accountsCreated": 1,
"membershipsCreated": 7,
"results": [
{
"domain": "acme.com",
"status": "created",
"reason": null,
"account": {
"id": "acc_abc123",
"externalId": "acme.com",
"name": "Acme",
"domain": "acme.com",
"attributes": {},
"memberCount": 7,
"lastEventAt": null,
"createdAt": "2026-09-23T10:00:00.000Z",
"updatedAt": "2026-09-23T10:00:00.000Z"
},
"contactCount": 7,
"membersAdded": 7,
"truncated": false
},
{
"domain": "globex.io",
"status": "skipped",
"reason": "no_contacts",
"account": null,
"contactCount": 0,
"membersAdded": 0,
"truncated": false
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "`gmail.com` is a personal or disposable email domain, not an organization."
}
```
```json 422 theme={null}
{ "success": false, "error": "Expected array with at least 1 item" }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 409 theme={null}
{
"success": false,
"error": "Contacts in this workspace already use the custom attribute `account.plan`. ..."
}
```
# List Account Suggestions
Source: https://docs.sequenzy.com/api-reference/accounts/suggestions-list
GET /api/v1/account-suggestions
Suggest accounts from work email domains your contacts share
Finds work email domains that several of your contacts share, such as seven contacts at `acme.com`, so you can turn them into accounts. Nothing is created until you [accept a suggestion](/api-reference/accounts/suggestions-accept). See [Accounts](/concepts/accounts).
These are skipped:
* Personal and disposable email providers, such as `gmail.com`, `outlook.com` and `yopmail.com`
* Your own sending domains and their subdomains
* Domains where any contact already belongs to an account, for example through an organization ID sync
* Domains an account already uses as its domain or external ID
Requires the `subscribers:read` scope.
## Request
Contacts a domain needs to be suggested. Values outside 2 to 1000 are clamped,
and non-numeric values use the default.
Number of suggestions, largest domains first. Values outside 1 to 100 are
clamped.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/account-suggestions?minContacts=3" \
-H "Authorization: Bearer API_KEY"
```
## Response
Suggested accounts, largest first.
The shared work email domain.
The name the account gets when you accept it, derived from the domain.
Contacts at the domain.
Up to 10 of those contacts' emails, alphabetical, so you can check who
is behind the suggestion before accepting it.
```json 200 theme={null}
{
"success": true,
"suggestions": [
{
"domain": "acme.com",
"name": "Acme",
"contactCount": 7,
"sampleEmails": ["ana@acme.com", "ben@acme.com", "chris@acme.com"]
},
{
"domain": "globex.io",
"name": "Globex",
"contactCount": 4,
"sampleEmails": ["dana@globex.io", "eli@globex.io"]
}
]
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
# Update Account
Source: https://docs.sequenzy.com/api-reference/accounts/update
PATCH /api/v1/accounts/{externalId}
Update an existing account's name, domain or attributes
Updates an existing account. Returns 404 when it does not exist; use [Create or Update Account](/api-reference/accounts/upsert) to upsert.
## Request
Your organization ID.
Display name. `null` clears it.
Primary domain. `null` clears it.
Attributes to merge. `null` values delete keys.
Replace all stored attributes instead of merging.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/accounts/org_123" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{ "attributes": { "plan": "team", "seats": null } }'
```
## Responses
```json 200 theme={null}
{ "success": true, "account": { "id": "acc_abc123", "externalId": "org_123", "name": "Acme", "attributes": { "plan": "team" }, "memberCount": 2 } }
```
```json 400 theme={null}
{ "success": false, "error": "Accounts support at most 100 attributes." }
```
```json 404 theme={null}
{ "success": false, "error": "Account not found" }
```
# Create or Update Account
Source: https://docs.sequenzy.com/api-reference/accounts/upsert
POST /api/v1/accounts
Upsert an account by your own organization ID
Creates the account when `externalId` is new, otherwise updates it. Attributes merge into the stored attributes: send `null` to delete a key, or `replaceAttributes: true` to replace them all. Attribute changes fan out to every member for segments and `{{account.*}}` merge tags.
Your first account turns Accounts on for the workspace. If your contacts already carry a custom attribute named `account` or `account.`, this returns `409` until you rename it, because account data takes over the `account.*` names in segments.
## Request
Your organization ID. Case-sensitive, unique per workspace, up to 255
characters.
Display name. `null` clears it.
Primary domain, normalized to a hostname (`https://www.acme.com/` becomes
`acme.com`). `null` clears it.
Up to 100 attributes such as `plan`, `seats`, `mrr`, `trialEndsAt`. Values may
be strings, numbers, booleans, arrays of those, or nested objects (flattened
to dotted names). Keys starting with `internal__` are reserved.
Replace all stored attributes instead of merging.
Up to 100 contacts to add in the same call. Each item takes `email` or
`externalId`, an optional `role` (`owner`, `admin`, `member`), `firstName` and
`lastName`. New emails create the contact; external-ID-only members must
already exist. Member roles and identities are checked before changing the
account or adding contacts.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/accounts" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "org_123",
"name": "Acme",
"domain": "acme.com",
"attributes": { "plan": "pro", "seats": 5, "trialEndsAt": "2026-09-20" },
"members": [{ "email": "jane@acme.com", "role": "owner" }]
}'
```
## Responses
```json 201 theme={null}
{
"success": true,
"created": true,
"account": {
"id": "acc_abc123",
"externalId": "org_123",
"name": "Acme",
"domain": "acme.com",
"attributes": { "plan": "pro", "seats": 5, "trialEndsAt": "2026-09-20" },
"memberCount": 1,
"lastEventAt": null,
"createdAt": "2026-09-04T09:00:00.000Z",
"updatedAt": "2026-09-04T09:00:00.000Z"
},
"members": [
{ "subscriberId": "sub_1", "email": "jane@acme.com", "externalId": null, "role": "owner", "created": true }
]
}
```
```json 200 theme={null}
{
"success": true,
"created": false,
"account": {
"id": "acc_abc123",
"externalId": "org_123",
"attributes": { "plan": "pro" }
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Account `domain` must be a hostname such as `acme.com`."
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 409 theme={null}
{
"success": false,
"error": "Contacts in this workspace already use the custom attribute `account.plan`. Accounts reserve the `account` attribute name for organization data, so turning Accounts on would stop those contact attributes from updating in segments. Rename them (for example to `company.plan`), then try again."
}
```
# Get Campaign Email Client Breakdown
Source: https://docs.sequenzy.com/api-reference/analytics/campaign-email-clients
GET /api/v1/metrics/campaigns/{campaignId}/clients
See which mail clients and devices opened a campaign.
Returns the mail clients and device types that opened one email campaign, as shares of its unique opens. The breakdown covers the whole campaign.
Each email send counts once, attributed to the client of its first open. Test sends are excluded, and detected email-security scanners are excluded unless you set `includeMachineEngagement=true`. For every email in a time window, use [Get Email Client Breakdown](/api-reference/analytics/email-clients).
## Path Parameters
The ID of an email campaign. SMS campaigns return `400`.
## Query Parameters
Optional recipient mailbox provider filter, for example `gmail`, `microsoft`,
`yahoo`, or `icloud`.
Set to `true` to include detected scanner, preview, and tracked asset opens.
## Response Fields
The campaign the breakdown covers.
Unique opens of the campaign. `0` when nothing was opened, in which case
`clients` and `devices` are empty arrays.
Opens that came through the Gmail or Yahoo image proxies or Apple Mail Privacy
Protection, which hide the reader's device.
Mail clients, most opens first. Each item has `key`, `label`, `opens`, and
`share` (percentage of `totalOpens`, not rounded). `key` is one of
`apple_mail`, `gmail`, `outlook`, `yahoo_mail`, `thunderbird`,
`android_mail_app`, `web_browser`, or `other`.
Device types, most opens first. `key` is one of `desktop`, `mobile`, `tablet`,
or `unknown`.
`mailboxProvider` is echoed back when you provide it.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/clients" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "camp_abc123",
"totalOpens": 420,
"privacyProxyOpens": 310,
"clients": [
{ "key": "apple_mail", "label": "Apple Mail", "opens": 210, "share": 50 },
{ "key": "gmail", "label": "Gmail", "opens": 168, "share": 40 },
{ "key": "outlook", "label": "Outlook", "opens": 42, "share": 10 }
],
"devices": [
{ "key": "unknown", "label": "Unknown", "opens": 310, "share": 73.80952380952381 },
{ "key": "mobile", "label": "Mobile", "opens": 70, "share": 16.666666666666668 },
{ "key": "desktop", "label": "Desktop", "opens": 40, "share": 9.523809523809524 }
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "This is an SMS campaign. SMS campaigns are managed from the dashboard; this endpoint only supports email campaigns."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
# List Campaign Events
Source: https://docs.sequenzy.com/api-reference/analytics/campaign-events
GET /api/v1/metrics/campaigns/{campaignId}/events
Retrieve paginated raw email events for a campaign.
Returns a paginated raw event stream for a campaign. The endpoint defaults to `delivery` events so you can audit delivered recipients, and can be expanded with `eventTypes` for opens, clicks, bounces, complaints, unsubscribes, sends, and delivery delays.
## Path Parameters
The ID of the campaign.
## Query Parameters
Comma-separated event types. Supported values: `send`, `delivery`, `bounce`,
`complaint`, `open`, `click`, `unsubscribe`, `delivery_delay`,
`transport_failure`.
Single event type convenience alias. If both `eventType` and `eventTypes` are
provided, both are included.
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Page number for pagination.
Events per page (max 500).
Set to `true` to include detected scanner, preview, and tracked asset
open/click events when requesting engagement event types.
## Common Queries
### List delivered recipients for a campaign
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/events?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Include clicks with deliveries
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/events?eventTypes=delivery,click&period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "camp_abc123",
"eventTypes": ["delivery"],
"events": [
{
"id": "evt_abc123",
"emailSendId": "send_abc123",
"companyId": "company_123",
"campaignId": "camp_abc123",
"transactionalEmailId": null,
"subscriberId": "sub_123",
"email": "user@example.com",
"emailName": "February Newsletter",
"emailType": "campaign",
"abTestId": null,
"abTestVariantId": null,
"eventType": "delivery",
"bounceType": null,
"bounceSubType": null,
"complaintType": null,
"clickedUrl": null,
"ipAddress": null,
"userAgent": null,
"countryCode": "US",
"metadata": {},
"eventTime": "2026-02-14T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 100,
"total": 1,
"totalPages": 1
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid event type 'purchase'. Must be one of: send, delivery, bounce, complaint, open, click, unsubscribe, delivery_delay, transport_failure"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Campaign not found" }
```
# Get Campaign Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/campaign-metrics
GET /api/v1/metrics/campaigns/{campaignId}
Retrieve engagement metrics for a specific campaign.
Returns engagement metrics (sends, deliveries, bounces, opens, clicks, replies, unsubscribes) plus attributed conversions, revenue, a per-link click breakdown, and any Poll or NPS survey summaries for a single campaign. Optionally filter engagement metrics by time period. Test sends are excluded from these performance metrics, attributed revenue, and poll results by default so previews do not skew results, but they still count toward send usage and quota. Open and click metrics also exclude detected email-security scanners and tracked brand assets by default.
`GET /api/v1/campaigns/{campaignId}/stats` is kept as a backward-compatible alias for this endpoint.
## Path Parameters
The ID of the campaign.
## Query Parameters
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Optional recipient mailbox provider filter, e.g. `gmail`, `microsoft`,
`yahoo`, `icloud`, `orange`. Scopes engagement metrics and the clicked-link
breakdown to recipients whose address is classified to that provider.
Provider-filtered responses report `replies`, `conversions`, and
`revenueCents` as `0` because those metrics cannot be segmented per provider.
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in engagement metrics.
## How a period is applied
When you filter with `period` or `start`/`end`, the response is a funnel over
the sends made inside that window, not a log of events that happened inside it.
`sent` counts those sends, and every delivery, engagement, and reply count is
attributed to one of them - including a reply or an open that arrives after the
window closes. Activity on sends that predate the window is excluded, so
`opened <= delivered <= sent` always holds and no rate can exceed 100%.
`conversions` and `revenueCents` are the exception: they use the goal's own
last-touch attribution window rather than the send cohort.
`opened` and `clicked` are unique counts deduplicated by email send: one
recipient opening the same email five times counts once. Rates divide by
`rateDenominator` (`delivered`, falling back to `sent`), which is returned
alongside `rateDenominatorBasis` so you can reproduce the arithmetic.
## Response Fields
| Field | Type | Description |
| - | - | - |
| `sent` | `number` | Emails sent in the period |
| `delivered` | `number` | Emails delivered |
| `bounced` | `number` | Emails that bounced |
| `opened` | `number` | Unique opens |
| `clicked` | `number` | Unique clicks |
| `replies` | `number` | Inbound replies captured for this campaign |
| `unsubscribed` | `number` | Unsubscribes |
| `deliveryRate` | `number` | Delivery rate percentage |
| `bounceRate` | `number` | Bounce rate percentage |
| `openRate` | `number` | Open rate percentage |
| `clickRate` | `number` | Click rate percentage |
| `replyRate` | `number` | Reply rate percentage |
| `unsubscribeRate` | `number` | Unsubscribe rate percentage |
| `rateDenominator` | `number` | The number every engagement rate divides by |
| `rateDenominatorBasis` | `string` | Which field `rateDenominator` came from: `delivered`, `sent`, or `none` |
| `conversions` | `number` | Goal conversions attributed to this campaign (last-touch, 24h window) |
| `revenueCents` | `number` | Attributed revenue in cents from purchase events (`saas.purchase` and `ecommerce.order_placed`) |
| `clickedLinks` | `array` | Top-level per-link click breakdown; omitted when the campaign has no tracked link clicks |
| `polls` | `array` | Top-level Poll/NPS summaries; omitted when the campaign has no responses |
Each `clickedLinks` item includes the destination `url`, the number of `clicks` on it, and `percentage` - that link's share of every recorded link click. Links are ordered by click count and capped at the top 20. Like polls, the breakdown covers the campaign's lifetime clicks even when engagement metrics are filtered with `period`, `start`, or `end`. Scanner and asset clicks are excluded unless `includeMachineEngagement=true`.
Each `polls` item includes `blockId`, `variant`, `question`, `attributeKey`, `totalResponses`, and an `answers` distribution. Every answer includes its human-readable `answer` label and stable stored `value`; use `value` as the identifier when labels can change. NPS items also include `nps.score`, `nps.average`, and promoter/passive/detractor counts. Single-answer polls count each subscriber once per block using their latest answer. Multi-select polls additionally set `allowMultiple: true`, count each subscriber once under every option in their latest saved selection, and use the number of respondents as `totalResponses`, so answer percentages can add up past 100%. Poll summaries are lifetime results for the campaign even when engagement metrics are filtered with `period`, `start`, or `end`.
`attributeKey` is where each subscriber's current/latest response is stored. A later poll can overwrite it when the same key is reused, so it is not a historical respondent identifier. To find the exact subscribers behind a campaign count, create a segment with field `pollResponse`, operator `is`, and a JSON value scoped to this campaign and the summary's `blockId`:
```json theme={null}
{
"v": 1,
"campaignId": "camp_abc123",
"blockId": "poll_1",
"match": { "kind": "answer", "value": "loved" }
}
```
For an NPS split, use `match` value `{"kind":"npsBucket","bucket":"detractors"}`; the other buckets are `promoters` and `passives`.
## Common Queries
### How did our February newsletter perform?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Without time filters, you get lifetime metrics for the campaign.
### How many opens in the first 24 hours after send?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123?period=24h" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Useful to gauge initial engagement before the campaign matures.
### Compare campaign performance over a specific week
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123?start=2026-02-01T00:00:00Z&end=2026-02-08T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Who opened or clicked in this campaign?
Use the [Get Recipients](/api-reference/analytics/recipients) endpoint with a `campaignId` filter to see individual recipient engagement:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?campaignId=camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "camp_abc123",
"stats": {
"sent": 1200,
"delivered": 1190,
"bounced": 10,
"opened": 420,
"clicked": 85,
"replies": 24,
"unsubscribed": 2,
"deliveryRate": 99.17,
"bounceRate": 0.83,
"openRate": 35.29,
"clickRate": 7.14,
"replyRate": 2.02,
"unsubscribeRate": 0.17,
"conversions": 12,
"revenueCents": 84500
},
"clickedLinks": [
{
"url": "https://example.com/pricing?utm_source=email",
"clicks": 52,
"percentage": 61.18
},
{
"url": "https://example.com/blog/launch",
"clicks": 33,
"percentage": 38.82
}
],
"polls": [
{
"blockId": "poll-nps-1",
"variant": "nps",
"question": "How likely are you to recommend us?",
"attributeKey": "nps_score",
"totalResponses": 3,
"answers": [
{ "answer": "0", "value": "0", "responses": 1, "percentage": 33.3 },
{ "answer": "1", "value": "1", "responses": 0, "percentage": 0 },
{ "answer": "2", "value": "2", "responses": 0, "percentage": 0 },
{ "answer": "3", "value": "3", "responses": 0, "percentage": 0 },
{ "answer": "4", "value": "4", "responses": 0, "percentage": 0 },
{ "answer": "5", "value": "5", "responses": 0, "percentage": 0 },
{ "answer": "6", "value": "6", "responses": 0, "percentage": 0 },
{ "answer": "7", "value": "7", "responses": 0, "percentage": 0 },
{ "answer": "8", "value": "8", "responses": 0, "percentage": 0 },
{ "answer": "9", "value": "9", "responses": 2, "percentage": 66.7 },
{ "answer": "10", "value": "10", "responses": 0, "percentage": 0 }
],
"nps": {
"score": 33,
"average": 6,
"promoters": 2,
"passives": 0,
"detractors": 1
}
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid period. Must be one of: 1h, 24h, 7d, 30d, 90d"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Campaign not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# List Campaign Poll Responses
Source: https://docs.sequenzy.com/api-reference/analytics/campaign-poll-responses
GET /api/v1/metrics/campaigns/{campaignId}/poll-responses
Retrieve individual Poll and NPS responses for a campaign.
Returns one row per respondent per Poll or NPS block in a campaign, newest answer first, with the answer, its stored value, the subscriber attribute the answer was saved to, and the response time.
The [campaign metrics endpoint](/api-reference/analytics/campaign-metrics) reports the answer distribution; this endpoint reports who answered what and when. Only each subscriber's latest answer per block is returned, so the counts match the `polls` summaries.
Poll answers are also written to a subscriber attribute, but that attribute carries no response time and holds the latest answer to any email, so respondents for one campaign cannot be reconstructed from it.
Also available at `GET /api/v1/campaigns/{campaignId}/poll-responses`.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Path Parameters
The ID of the campaign. For a sequence email step, pass the step's automation
node ID.
## Query Parameters
Restrict results to one poll block. Block IDs come from the `polls` array of
the campaign metrics endpoint.
Page number for pagination.
Responses per page (max 500).
## Common Queries
### List everyone who answered a campaign poll
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/poll-responses?limit=500" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Scope to one poll block
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/poll-responses?blockId=poll_1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response Fields
Multi-select polls set `allowMultiple` to `true` and list every selected option in `answers`, with the stored values in the parallel `values` array. `email` is `null` when the subscriber has since been deleted.
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "camp_abc123",
"blockId": "poll_1",
"responses": [
{
"subscriberId": "sub_123",
"email": "user@example.com",
"externalId": null,
"firstName": "Ana",
"lastName": null,
"blockId": "poll_1",
"variant": "options",
"question": "Did you get your sample?",
"attributeKey": "sample_received",
"allowMultiple": false,
"answers": ["Yes"],
"values": ["yes"],
"respondedAt": "2026-08-08T11:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 100,
"total": 1,
"totalPages": 1
}
}
```
```json 400 theme={null}
{ "success": false, "error": "'limit' must be a positive integer" }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Campaign not found" }
```
# Get Email Client Breakdown
Source: https://docs.sequenzy.com/api-reference/analytics/email-clients
GET /api/v1/metrics/clients
See which mail clients and devices open your emails.
Returns the mail clients (Apple Mail, Gmail, Outlook and others) and device types that opened your emails, as shares of unique opens. Use it to decide which clients to test your designs in.
Each email send counts once, attributed to the client of its first open, so a subscriber who reopens an email on another device is not counted twice. Test sends are excluded, and detected email-security scanners are excluded unless you set `includeMachineEngagement=true`. For one campaign, use [Get Campaign Email Client Breakdown](/api-reference/analytics/campaign-email-clients).
## Query Parameters
Sliding window over open times. One of: `1h`, `24h`, `7d`, `30d`, `90d`.
Ignored when `start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Optional structural filter: `campaign`, `sequence`, or `transactional`.
Marketer-role personal keys only see campaign and sequence email and receive
`403` for `transactional`.
Optional recipient mailbox provider filter, for example `gmail`, `microsoft`,
`yahoo`, or `icloud`.
Set to `true` to include detected scanner, preview, and tracked asset opens.
## Response Fields
Applied period, or `custom` when `start` and `end` were provided.
Unique opens the breakdown covers. `0` when nothing was opened, in which case
`clients` and `devices` are empty arrays.
Opens that came through the Gmail or Yahoo image proxies or Apple Mail Privacy
Protection. These report the client but not the reader's device, so they are
counted under the `unknown` device.
Mail clients, most opens first. Each item has `key`, `label`, `opens`, and
`share` (percentage of `totalOpens`, not rounded). `key` is one of
`apple_mail`, `gmail`, `outlook`, `yahoo_mail`, `thunderbird`,
`android_mail_app`, `web_browser`, or `other`. `android_mail_app` covers
Android apps that show mail in a WebView, such as Samsung Email. `web_browser`
covers webmail, the new Outlook for Windows, and opens recorded from a link
click without a tracked open. Some apps built on Apple's WebKit, such as
Outlook for Mac, are reported as `apple_mail`. `other` covers unrecognized and
missing user agents.
Device types, most opens first, with the same item fields. `key` is one of
`desktop`, `mobile`, `tablet`, or `unknown`.
`emailType`, `mailboxProvider`, `start`, and `end` are echoed back when you
provide them.
Recipients who never open, or whose client blocks images, are not represented,
so read the shares as "of the people who opened".
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/clients?period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"period": "30d",
"totalOpens": 200,
"privacyProxyOpens": 150,
"clients": [
{ "key": "gmail", "label": "Gmail", "opens": 100, "share": 50 },
{ "key": "apple_mail", "label": "Apple Mail", "opens": 80, "share": 40 },
{ "key": "outlook", "label": "Outlook", "opens": 20, "share": 10 }
],
"devices": [
{ "key": "unknown", "label": "Unknown", "opens": 150, "share": 75 },
{ "key": "desktop", "label": "Desktop", "opens": 30, "share": 15 },
{ "key": "mobile", "label": "Mobile", "opens": 20, "share": 10 }
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid emailType. Must be one of: campaign, transactional, sequence"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "Personal API keys inherit your workspace role, and the marketer role cannot use this route (it belongs to workspace settings, integrations, or transactional email). Ask the workspace owner for admin access, or use a company API key issued for this workspace."
}
```
# List Per-Email Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/email-metrics
GET /api/v1/metrics/emails
Compare every campaign and sequence email step in one call, with totals across all matching emails.
Returns one row per email - each campaign and each individual sequence email
step - with its own delivery funnel, attributed conversions, and revenue. Where
[Get Sequence Metrics](/api-reference/analytics/sequence-metrics) answers "how
did this one sequence's steps perform", this endpoint answers cross-sequence
questions such as "how many step-4 emails went out across these sequences"
without one request per sequence.
Counts come from retained event storage, not the 14-day delivery history behind
[List Sent Emails](/api-reference/email-sends/list), so old sends stay
answerable. Sequence rows report the same numbers as the matching entry in the
sequence metrics `steps` array, because both use the same step ordering and the
same counting rules. Test sends are excluded, and detected scanner opens and
clicks are excluded unless you pass `includeMachineEngagement`.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Query Parameters
Restrict to `campaign` or `sequence`. Defaults to both. Implied as `sequence`
when `sequenceId` or `step` is set, and as `campaign` when `campaignId` is
set.
Comma-separated sequence IDs to restrict the breakdown to. Omit for every
sequence in the account. Cannot be combined with `campaignId` or
`emailType=campaign`.
Comma-separated campaign IDs to restrict the breakdown to. Cannot be combined
with `sequenceId`, `step`, or `emailType=sequence`.
Keep only sequence emails at this 1-based position, counted in graph order per
sequence. `step=4` keeps the fourth email of every matching sequence. Cannot
be combined with `emailType=campaign`.
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided. Omit both for all-time counts.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Sort field: `sent`, `delivered`, `opened`, `clicked`, `openRate`, `clickRate`,
`unsubscribed`, `conversions`, `revenue`, `step`, or `name`.
Sort order: `asc` or `desc`.
Page number for pagination.
Emails per page (max 500).
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in engagement metrics.
## Response Fields
| Field | Type | Description |
| - | - | - |
| `emails` | `array` | One entry per matching email, ordered by `sort` and `order` |
| `totals` | `object` | Summed funnel across every matching email, not just this page |
| `pagination` | `object` | `page`, `limit`, `total`, and `totalPages` for the email list |
**Email object**:
| Field | Type | Description |
| - | - | - |
| `emailType` | `string` | `campaign` or `sequence` |
| `emailId` | `string` | Campaign ID for campaigns, automation node ID for sequence emails |
| `name` | `string` | Campaign name, or the step's subject line falling back to its node label |
| `campaignId` | `string\|null` | Campaign ID, or `null` for sequence emails |
| `sequenceId` | `string\|null` | Sequence ID, or `null` for campaigns |
| `sequenceName` | `string\|null` | Name of the sequence this step belongs to |
| `automationNodeId` | `string\|null` | Node ID of this step; pass it to sequence events or sent emails to see recipients |
| `step` | `number\|null` | 1-based position of this email in its sequence, or `null` for campaigns |
| `stats` | `object` | Delivery funnel for this email alone |
| `conversions` | `number` | Goal conversions attributed to this email |
| `revenueCents` | `number` | Attributed revenue in cents |
`totals` carries the same funnel fields plus `emails` (the number of matching
emails), `conversions`, and `revenueCents`. Totals always cover the full
filtered set, so paging never changes the answer to "how many went out".
## Common Queries
### How many step-4 emails have we sent across all sequences?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/emails?step=4" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Read `totals.sent`. The `emails` array breaks the same number down per sequence.
### Same question, but only for two specific sequences
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/emails?sequenceId=seq_abc123,seq_def456&step=4" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Which sequence emails have the best open rate?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/emails?emailType=sequence&sort=openRate&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Which emails drove the most revenue last quarter?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/emails?period=90d&sort=revenue" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Who received one of these steps?
Take an `automationNodeId` from the response and pass it to
[List Sequence Events](/api-reference/analytics/sequence-events) for the full
retained event stream, or to
[List Sent Emails](/api-reference/email-sends/list) for the last 14 days of
per-recipient delivery rows.
## Responses
```json 200 theme={null}
{
"success": true,
"step": 4,
"sort": "sent",
"order": "desc",
"emails": [
{
"emailType": "sequence",
"emailId": "node_email_4_winback",
"name": "Still thinking it over?",
"campaignId": null,
"sequenceId": "seq_def456",
"sequenceName": "Winback",
"automationNodeId": "node_email_4_winback",
"step": 4,
"stats": {
"sent": 820,
"delivered": 811,
"bounced": 9,
"opened": 402,
"clicked": 76,
"replies": 0,
"unsubscribed": 3,
"rateDenominator": 811,
"rateDenominatorBasis": "delivered",
"deliveryRate": 98.9,
"bounceRate": 1.1,
"openRate": 49.57,
"clickRate": 9.37,
"replyRate": 0,
"unsubscribeRate": 0.37
},
"conversions": 14,
"revenueCents": 128400
},
{
"emailType": "sequence",
"emailId": "node_email_4_onboarding",
"name": "Your first week recap",
"campaignId": null,
"sequenceId": "seq_abc123",
"sequenceName": "Onboarding",
"automationNodeId": "node_email_4_onboarding",
"step": 4,
"stats": {
"sent": 512,
"delivered": 508,
"bounced": 4,
"opened": 289,
"clicked": 41,
"replies": 0,
"unsubscribed": 1,
"rateDenominator": 508,
"rateDenominatorBasis": "delivered",
"deliveryRate": 99.22,
"bounceRate": 0.78,
"openRate": 56.89,
"clickRate": 8.07,
"replyRate": 0,
"unsubscribeRate": 0.2
},
"conversions": 6,
"revenueCents": 43200
}
],
"totals": {
"emails": 2,
"sent": 1332,
"delivered": 1319,
"bounced": 13,
"opened": 691,
"clicked": 117,
"replies": 0,
"unsubscribed": 4,
"rateDenominator": 1319,
"rateDenominatorBasis": "delivered",
"deliveryRate": 99.02,
"bounceRate": 0.98,
"openRate": 52.39,
"clickRate": 8.87,
"replyRate": 0,
"unsubscribeRate": 0.3,
"conversions": 20,
"revenueCents": 171600
},
"pagination": {
"page": 1,
"limit": 50,
"total": 2,
"totalPages": 1
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "step filters sequence emails and cannot be combined with emailType=campaign"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Get Account Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/metrics
GET /api/v1/metrics
Retrieve email engagement metrics plus explainable commerce AOV, LTV, and revenue forecasts.
Returns aggregated email engagement metrics (sends, deliveries, bounces, opens, clicks, replies, unsubscribes) across your entire account for a given time period, plus a live audience snapshot in top-level `subscriberCount` (every stored contact) and `activeSubscriberCount` (status=`active`). Those two fields are independent of `period` / `start` / `end`. Set `emailType=transactional` to isolate Send API and transactional SMTP traffic, including both direct and saved-template sends. Test sends are excluded from these performance metrics by default so previews do not skew results, but they still count toward send usage and quota. Open and click metrics also exclude detected email-security scanners and tracked brand assets by default.
When no `emailType` filter is used, the optional top-level `commerceForecast` is calculated separately from the
requested email period and served from the latest background-computed snapshot.
It uses up to two years of provider-neutral
`ecommerce.order_placed` history and returns predicted AOV, predicted 12-month
customer value, expected 90-day orders and revenue, confidence ranges, and the
exact eligibility reasons when a store is still learning. Forecasts require at
least 20 orders, 10 customers, 30 days of history, 3 repeat customers, and a
recent order within 45 days.
The field is omitted when no snapshot is available or the analytics store is
temporarily unavailable; `insufficient_data` only describes a successfully
evaluated store that has not met the requirements above.
`GET /api/v1/stats` is kept as a backward-compatible alias for this endpoint.
## Query Parameters
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601, e.g. `2026-02-01T00:00:00Z`). Must be
used with `end`.
End of custom time range (ISO 8601, e.g. `2026-02-14T00:00:00Z`). Must be used
with `start`. Max range: 90 days.
Optional structural traffic filter. One of: `campaign`, `transactional`, or
`sequence`. Use `transactional` for Send API and transactional SMTP metrics.
Optional recipient mailbox provider filter, e.g. `gmail`, `microsoft`,
`yahoo`, `icloud`, `orange`. Scopes engagement metrics to recipients whose
address is classified to that provider. Provider-filtered responses report
`replies` as `0` (replies cannot be segmented per provider) and omit the
commerce forecast. Events recorded before provider classification existed are
excluded while this filter is active.
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in engagement metrics.
## How the period is applied
These metrics are a funnel over the sends made inside the period, not a log of
events that happened inside it.
* `sent` counts the emails sent between `start` and `end`.
* Every other count is attributed to one of those sends. An open that arrives
three days after the send still counts toward the period the send belongs to,
and an open that arrives today for an email sent last month does not.
* `replies` follows the same rule, so `replyRate` divides replies earned by the
period's sends by that same cohort rather than by whatever happened to arrive
in the calendar window.
* `opened` and `clicked` are unique counts deduplicated by email send. One
recipient opening the same email five times counts once. They are not totals
of open events.
Because of this, `opened <= delivered <= sent` always holds and no rate can
exceed 100%. Activity on sends that predate the period is excluded rather
than counted against a smaller `sent` figure.
## Response Fields
| Field | Type | Description |
| - | - | - |
| `sent` | `number` | Emails sent in the period |
| `delivered` | `number` | Emails delivered (capped at sent) |
| `bounced` | `number` | Emails that bounced |
| `opened` | `number` | Unique opens (deduplicated by email send) |
| `clicked` | `number` | Unique clicks (deduplicated by email send) |
| `replies` | `number` | Inbound replies captured for the period's sends |
| `unsubscribed` | `number` | Unsubscribes attributed to the period's sends |
| `rateDenominator` | `number` | The number every engagement rate below divides by |
| `rateDenominatorBasis` | `string` | Which field `rateDenominator` came from: `delivered`, `sent`, or `none` |
| `deliveryRate` | `number` | Delivery rate percentage (delivered / sent) |
| `bounceRate` | `number` | Bounce rate percentage (bounced / sent) |
| `openRate` | `number` | Open rate percentage (opened / `rateDenominator`) |
| `clickRate` | `number` | Click rate percentage (clicked / `rateDenominator`) |
| `replyRate` | `number` | Reply rate percentage (replies / `rateDenominator`) |
| `unsubscribeRate` | `number` | Unsubscribe rate percentage (unsubscribed / `rateDenominator`) |
`rateDenominator` is `delivered`, falling back to `sent` when no delivery
events were recorded, and `0` when nothing was sent. It is returned so you can
reproduce any rate exactly instead of guessing the denominator.
### Commerce forecast
This optional object is omitted when a current forecast snapshot is unavailable.
| Field | Type | Description |
| - | - | - |
| `commerceForecast.status` | `string` | `ready` or `insufficient_data` |
| `commerceForecast.currency` | `string \| null` | Primary order currency used by the forecast |
| `commerceForecast.confidence` | `string \| null` | `low`, `medium`, or `high` when ready |
| `commerceForecast.history` | `object` | Order, customer, repeat, freshness, and history counts |
| `commerceForecast.actual` | `object` | Observed AOV, customer value, and repeat rate |
| `commerceForecast.forecast.predictedAverageOrderValueCents` | `number` | Predicted order value in minor currency units |
| `commerceForecast.forecast.predictedCustomerValue365DaysCents` | `number` | Predicted 12-month customer value |
| `commerceForecast.forecast.expectedOrders90Days` | `number` | Expected order count over the next 90 days |
| `commerceForecast.forecast.expectedRevenue90DaysCents` | `number` | Expected revenue over the next 90 days |
| `commerceForecast.eligibility.reasons` | `array` | Requirements that prevent a forecast from running |
## Common Queries
### How many subscribers do we have?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?period=7d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Read top-level `subscriberCount` and `activeSubscriberCount`. They are a live audience snapshot and do not change with `period`.
### How many emails did we send in the last 7 days?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?period=7d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Check the `sent` field in the response.
### What's our overall open rate this month?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?start=2026-02-01T00:00:00Z&end=2026-02-28T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Check the `openRate` field. It is calculated as `opened / rateDenominator`,
where `rateDenominator` is `delivered` (or `sent` when no delivery events were
recorded). Both are returned so you can verify the arithmetic.
### How are we doing in the last 24 hours?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?period=24h" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Useful for daily monitoring dashboards or Slack alerts.
### What are our Send API open and click rates?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?period=30d&emailType=transactional" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The response includes `opened`, `clicked`, `openRate`, and `clickRate` for
transactional-origin messages. To inspect one delivery, use its `emailSendId`
with `GET /api/v1/email-sends/{emailSendId}`. To find IDs by subject, recipient,
or delivery status, use `GET /api/v1/email-sends`. For one saved transactional
email's aggregate rates, use
`GET /api/v1/metrics/transactional/{idOrSlug}`.
### Sync daily metrics to our data warehouse
Use fixed time ranges to pull yesterday's metrics on a cron schedule:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics?start=2026-02-13T00:00:00Z&end=2026-02-14T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Fixed ranges are idempotent: safe to retry without double-counting.
## Responses
```json 200 theme={null}
{
"success": true,
"period": "7d",
"subscriberCount": 12400,
"activeSubscriberCount": 11880,
"stats": {
"sent": 3400,
"delivered": 3380,
"bounced": 20,
"opened": 1020,
"clicked": 340,
"replies": 68,
"unsubscribed": 5,
"rateDenominator": 3380,
"rateDenominatorBasis": "delivered",
"deliveryRate": 99.41,
"bounceRate": 0.59,
"openRate": 30.18,
"clickRate": 10.06,
"replyRate": 2.01,
"unsubscribeRate": 0.15
},
"commerceForecast": {
"status": "ready",
"modelVersion": "cohort-rfm-v1",
"currency": "USD",
"confidence": "medium",
"history": {
"orderCount": 84,
"customerCount": 51,
"repeatCustomerCount": 18,
"historyDays": 180,
"daysSinceLastOrder": 1,
"excludedCurrencyOrderCount": 0
},
"forecast": {
"predictedAverageOrderValueCents": 7200,
"predictedCustomerValue365DaysCents": 18400,
"expectedOrders90Days": 68.5,
"expectedRevenue90DaysCents": 493200
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid period. Must be one of: 1h, 24h, 7d, 30d, 90d"
}
```
```json 400 Invalid email type theme={null}
{
"success": false,
"error": "Invalid emailType. Must be one of: campaign, transactional, sequence"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Get Recipient Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/recipients
GET /api/v1/metrics/recipients
Retrieve paginated recipient engagement data including opens, clicks, and unsubscribes.
Returns a paginated list of recipients who received emails, along with their engagement events (opens, clicks, unsubscribes). Use this endpoint to sync engagement data to your own database or to look up how a specific person interacted with your emails. Test sends are excluded from recipient engagement results by default so previews do not skew results, but they still count toward send usage and quota. Open and click events also exclude detected email-security scanners and tracked brand assets by default.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Query Parameters
Filter to a single recipient by email address.
Filter to recipients of a specific campaign. Cannot be combined with
`sequenceId`.
Filter to recipients of a specific sequence. Cannot be combined with
`campaignId`.
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Page number for pagination.
Recipients per page (max 100).
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in each recipient's engagement arrays.
## Response Fields
| Field | Type | Description |
| - | - | - |
| `recipients[].email` | `string` | Recipient email address |
| `recipients[].opened` | `array` | List of open events with `campaignId`, `subject`, and `at` timestamp |
| `recipients[].opened[].machine` | `boolean` | Whether the event is classified as bot/scanner activity |
| `recipients[].opened[].engagementQuality` | `string` | `human`, `machine`, or `asset` |
| `recipients[].opened[].classificationReasons` | `array` | Classification reason codes, such as `microsoft_office_scanner`, `managed_image_asset`, or `static_brand_asset` |
| `recipients[].clicked` | `array` | List of click events with `campaignId`, `subject`, `url`, and `at` timestamp |
| `recipients[].clicked[].machine` | `boolean` | Whether the event is classified as bot/scanner activity |
| `recipients[].clicked[].engagementQuality` | `string` | `human`, `machine`, or `asset` |
| `recipients[].clicked[].classificationReasons` | `array` | Classification reason codes, such as `microsoft_office_scanner`, `managed_image_asset`, or `static_brand_asset` |
| `recipients[].unsubscribed` | `boolean` | Whether the recipient unsubscribed |
| `pagination.page` | `number` | Current page |
| `pagination.limit` | `number` | Results per page |
| `pagination.total` | `number` | Total matching recipients |
| `pagination.totalPages` | `number` | Total pages available |
## Common Queries
### How did a specific user interact with our emails?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?email=user@example.com" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Returns all opens, clicks, and unsubscribe status for that person. Useful for support conversations or CRM enrichment.
### Who opened our latest campaign?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?campaignId=camp_abc123&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Page through the results to get all recipients. Check each recipient's `opened` array: those with entries opened the campaign.
### Who clicked a link in our onboarding sequence?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?sequenceId=seq_abc123&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Check each recipient's `clicked` array for entries. The `url` field shows which link they clicked.
### Who engaged in the last 24 hours?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?period=24h&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Sync engagement data to our database on a cron schedule
Pull yesterday's engagement data with a fixed time range:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?start=2026-02-13T00:00:00Z&end=2026-02-14T00:00:00Z&limit=100&page=1" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Increment `page` until `page > totalPages` to get all results. Fixed time ranges are idempotent: safe to retry.
## Responses
```json 200 theme={null}
{
"success": true,
"recipients": [
{
"email": "user@example.com",
"opened": [
{
"campaignId": "camp_abc123",
"subject": "February Newsletter",
"machine": false,
"engagementQuality": "human",
"classificationReasons": [],
"at": "2026-02-14T10:30:00.000Z"
}
],
"clicked": [
{
"campaignId": "camp_abc123",
"subject": "February Newsletter",
"url": "https://example.com/start",
"machine": false,
"engagementQuality": "human",
"classificationReasons": [],
"at": "2026-02-14T10:35:00.000Z"
}
],
"unsubscribed": false
},
{
"email": "other@example.com",
"opened": [],
"clicked": [],
"unsubscribed": true
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"totalPages": 1
}
}
```
```json 400 theme={null}
{ "success": false, "error": "Cannot filter by both campaignId and sequenceId" }
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Campaign not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# List Sequence Events
Source: https://docs.sequenzy.com/api-reference/analytics/sequence-events
GET /api/v1/metrics/sequences/{sequenceId}/events
Retrieve paginated raw email events for all email steps in a sequence.
Returns a paginated raw event stream for every email step in a sequence, or for a single step via `automationNodeId`. The endpoint defaults to `delivery` events and resolves the sequence to its email and A/B test nodes before querying event storage. Use `eventTypes` to include opens, clicks, bounces, complaints, unsubscribes, sends, and delivery delays.
This is the per-recipient event stream. For per-step totals, read the `steps`
array of [Get Sequence Metrics](/api-reference/analytics/sequence-metrics) or
call [List Per-Email Metrics](/api-reference/analytics/email-metrics) instead of
counting these pages.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Path Parameters
The ID of the sequence.
## Query Parameters
Scope the stream to one email step of this sequence. Take the node ID from the
`steps` array of the sequence metrics endpoint. A node that is not an email
step of this sequence returns `400`, so a mistyped ID never reads as "this
step sent nothing". Defaults to every email step, and `automationNodeIds` in
the response echoes what was queried.
Comma-separated event types. Supported values: `send`, `delivery`, `bounce`,
`complaint`, `open`, `click`, `unsubscribe`, `delivery_delay`,
`transport_failure`.
Single event type convenience alias. If both `eventType` and `eventTypes` are
provided, both are included.
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Page number for pagination.
Events per page (max 500).
Set to `true` to include detected scanner, preview, and tracked asset
open/click events when requesting engagement event types.
## Common Queries
### List delivered recipients for a sequence
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123/events?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Include opens and clicks for sequence email steps
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123/events?eventTypes=delivery,open,click&period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### List who received one specific step
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123/events?automationNodeId=node_email_4&limit=500" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"automationNodeIds": ["node_email_1", "node_email_2"],
"eventTypes": ["delivery"],
"events": [
{
"id": "evt_abc123",
"emailSendId": "send_abc123",
"companyId": "company_123",
"campaignId": "node_email_1",
"transactionalEmailId": null,
"subscriberId": "sub_123",
"email": "user@example.com",
"emailName": "Welcome",
"emailType": "sequence",
"abTestId": null,
"abTestVariantId": null,
"eventType": "delivery",
"bounceType": null,
"bounceSubType": null,
"complaintType": null,
"clickedUrl": null,
"ipAddress": null,
"userAgent": null,
"countryCode": "US",
"metadata": {},
"eventTime": "2026-02-14T10:30:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 100,
"total": 1,
"totalPages": 1
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid event type 'purchase'. Must be one of: send, delivery, bounce, complaint, open, click, unsubscribe, delivery_delay, transport_failure"
}
```
```json 400 theme={null}
{
"success": false,
"error": "automationNodeId 'node_other' is not an email step of sequence 'seq_abc123'"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Sequence not found" }
```
# Get Sequence Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/sequence-metrics
GET /api/v1/metrics/sequences/{sequenceId}
Retrieve engagement metrics for a specific sequence, including per-step breakdown and failure details.
Returns aggregate engagement metrics for a sequence (automation), a live
active/waiting enrollment breakdown by current node, and a per-step breakdown
showing how each email performed, including replies and reply rates. Each step
also includes recent failed subscribers and their failure reasons. Aggregate
and per-step performance metrics respect the time filter; `enrollmentCounts`
is always a point-in-time snapshot. Test sends are excluded from performance
metrics and attributed revenue by default so previews do not skew results, but
they still count toward send usage and quota. Open and click metrics also
exclude detected email-security scanners and tracked brand assets by default.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Path Parameters
The ID of the sequence.
## Query Parameters
Sliding time window. One of: `1h`, `24h`, `7d`, `30d`, `90d`. Ignored when
`start` and `end` are provided.
Start of custom time range (ISO 8601). Must be used with `end`.
End of custom time range (ISO 8601). Must be used with `start`. Max range: 90
days.
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in engagement metrics.
## How a period is applied
When you filter with `period` or `start`/`end`, the response is a funnel over
the sends made inside that window, not a log of events that happened inside it.
`sent` counts those sends, and every delivery, engagement, and reply count is
attributed to one of them - including a reply or an open that arrives after the
window closes. Activity on sends that predate the window is excluded, so
`opened <= delivered <= sent` always holds and no rate can exceed 100%.
`conversions` and `revenueCents` are the exception: they use the goal's own
last-touch attribution window rather than the send cohort.
`opened` and `clicked` are unique counts deduplicated by email send: one
recipient opening the same email five times counts once. Rates divide by
`rateDenominator` (`delivered`, falling back to `sent`), which is returned
alongside `rateDenominatorBasis` so you can reproduce the arithmetic.
## Response Fields
| Field | Type | Description |
| - | - | - |
| `sequenceId` | `string` | The sequence ID |
| `stats` | `object` | Aggregate stats across all steps (same shape as below) |
| `enrollmentCounts` | `object` | Live active/waiting enrollment counts by current node |
| `enrollmentSkipped` | `object` | Trigger matches where the contact could not be enrolled |
| `steps` | `array` | Per-step metrics, ordered by position |
**Stats object** (used for both aggregate and per-step):
| Field | Type | Description |
| - | - | - |
| `sent` | `number` | Emails sent |
| `delivered` | `number` | Emails delivered |
| `bounced` | `number` | Emails that bounced |
| `opened` | `number` | Unique opens |
| `clicked` | `number` | Unique clicks |
| `replies` | `number` | Inbound replies captured for the sequence or step |
| `unsubscribed` | `number` | Unsubscribes |
| `deliveryRate` | `number` | Delivery rate percentage |
| `bounceRate` | `number` | Bounce rate percentage |
| `openRate` | `number` | Open rate percentage |
| `clickRate` | `number` | Click rate percentage |
| `replyRate` | `number` | Reply rate percentage |
| `unsubscribeRate` | `number` | Unsubscribe rate percentage |
| `rateDenominator` | `number` | The number every engagement rate divides by |
| `rateDenominatorBasis` | `string` | Which field `rateDenominator` came from: `delivered`, `sent`, or `none` |
| `conversions` | `number` | Goal conversions attributed to this sequence (last-touch, 24h window) |
| `revenueCents` | `number` | Attributed revenue in cents from purchase events (`saas.purchase` and `ecommerce.order_placed`) |
The `conversions` and `revenueCents` fields appear on the top-level `stats` object only, not on per-step stats.
**Enrollment counts object**:
| Field | Type | Description |
| - | - | - |
| `active` | `number` | Active enrollment-token count |
| `waiting` | `number` | Waiting enrollment-token count |
| `total` | `number` | Active plus waiting enrollment-token count |
| `byCurrentNode` | `array` | The same counts grouped by the token's current node |
Each `byCurrentNode` entry contains `currentNodeId`, optional
`currentNodeType` and `currentNodeLabel`, `currentNodeMissing`, and the
`active`, `waiting`, and `total` counts. Missing nodes remain in the response
so the total always reconciles even after a live sequence graph is edited.
These counts represent enrollment runs, not necessarily distinct subscribers.
They are read live and do not use `period`, `start`, or `end`.
**Enrollment skipped object**:
| Field | Type | Description |
| - | - | - |
| `count` | `number` | Total trigger matches where the contact could not be enrolled |
| `byReason` | `object` | Skip counts keyed by reason (`unsubscribed`, `bounced`) |
Unsubscribed and bounced contacts never enter sequences, so a trigger match
for them is recorded as a skip instead of an enrollment. When the request has
no explicit time range, `enrollmentSkipped` covers the last 30 days.
**Steps array**:
| Field | Type | Description |
| - | - | - |
| `step` | `number` | Step number (1-indexed) |
| `nodeId` | `string` | Node ID for this email step |
| `subject` | `string\|null` | Email subject line |
| `stats` | `object` | Stats for this step |
| `failedCount` | `number` | Number of subscribers that failed at this step |
| `failedSubscribers` | `array` | Up to 20 most recent failed subscribers for this step |
**Failed subscriber object**:
| Field | Type | Description |
| - | - | - |
| `subscriberId` | `string` | Subscriber ID |
| `email` | `string\|null` | Subscriber email captured on the token |
| `failedAt` | `string` | When the token failed (ISO 8601) |
| `failedReason` | `string\|null` | Stored failure reason for the token |
## Common Queries
### How is our onboarding sequence performing overall?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Check the top-level `stats` for aggregate performance across all steps.
### How many enrollments are waiting at each stage right now?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123?period=7d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Read `enrollmentCounts.byCurrentNode`. The 7-day filter scopes historical
performance metrics only; the active/waiting counts still reflect the current
sequence state.
### Which step has the highest drop-off?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Compare `openRate` across the `steps` array. A sharp drop between consecutive steps signals where subscribers lose interest.
### How many people unsubscribed from the second email?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Look at `steps[1].stats.unsubscribed` (step 2, zero-indexed in the array). If the unsubscribe rate is high on a specific step, consider revising that email's content or timing.
### How has the sequence performed this month?
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123?start=2026-02-01T00:00:00Z&end=2026-02-28T00:00:00Z" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Both aggregate and per-step metrics will be scoped to this time range.
### How many step-4 emails went out across several sequences?
The `steps` array covers one sequence per request. To total or compare the same
step across many sequences in a single call, use
[List Per-Email Metrics](/api-reference/analytics/email-metrics):
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/emails?sequenceId=seq_abc123,seq_def456&step=4" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Step numbers and per-step counts match this endpoint's `steps` array, so the two
surfaces never disagree about which email is step 4.
### Who received one specific step?
Pass a `steps[].nodeId` to
[List Sequence Events](/api-reference/analytics/sequence-events) as
`automationNodeId` for the retained per-recipient event stream, or to
[List Sent Emails](/api-reference/email-sends/list) for the last 14 days of
delivery rows.
### Who engaged with this sequence?
Use the [Get Recipients](/api-reference/analytics/recipients) endpoint with a `sequenceId` filter:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/recipients?sequenceId=seq_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"stats": {
"sent": 122,
"delivered": 120,
"bounced": 2,
"opened": 80,
"clicked": 5,
"replies": 9,
"unsubscribed": 1,
"deliveryRate": 98.36,
"bounceRate": 1.64,
"openRate": 66.67,
"clickRate": 4.17,
"replyRate": 7.5,
"unsubscribeRate": 0.83,
"conversions": 4,
"revenueCents": 27800
},
"enrollmentCounts": {
"active": 4,
"waiting": 26,
"total": 30,
"byCurrentNode": [
{
"currentNodeId": "node_wait_3_days",
"currentNodeType": "logic_delay",
"currentNodeLabel": "Wait three days",
"currentNodeMissing": false,
"active": 0,
"waiting": 18,
"total": 18
},
{
"currentNodeId": "node_002",
"currentNodeType": "action_email",
"currentNodeLabel": "Quick tips to get started",
"currentNodeMissing": false,
"active": 4,
"waiting": 8,
"total": 12
}
]
},
"steps": [
{
"step": 1,
"nodeId": "node_001",
"subject": "Welcome to our platform",
"stats": { "sent": 60, "delivered": 59, "bounced": 1, "opened": 40, "clicked": 3, "replies": 6, "unsubscribed": 0, "deliveryRate": 98.33, "bounceRate": 1.67, "openRate": 67.8, "clickRate": 5.08, "replyRate": 10.17, "unsubscribeRate": 0 },
"failedCount": 0,
"failedSubscribers": []
},
{
"step": 2,
"nodeId": "node_002",
"subject": "Quick tips to get started",
"stats": { "sent": 44, "delivered": 43, "bounced": 1, "opened": 30, "clicked": 2, "replies": 3, "unsubscribed": 1, "deliveryRate": 97.73, "bounceRate": 2.27, "openRate": 69.77, "clickRate": 4.65, "replyRate": 6.98, "unsubscribeRate": 2.33 },
"failedCount": 2,
"failedSubscribers": [
{
"subscriberId": "sub_failed_001",
"email": "failed@example.com",
"failedAt": "2026-02-14T09:32:11.000Z",
"failedReason": "Email template ID is required"
},
{
"subscriberId": "sub_failed_002",
"email": "unknown@example.com",
"failedAt": "2026-02-14T09:11:42.000Z",
"failedReason": "Node node_002 not found"
}
]
},
{
"step": 3,
"nodeId": "node_003",
"subject": "Your first week recap",
"stats": { "sent": 18, "delivered": 18, "bounced": 0, "opened": 10, "clicked": 0, "replies": 0, "unsubscribed": 0, "deliveryRate": 100, "bounceRate": 0, "openRate": 55.56, "clickRate": 0, "replyRate": 0, "unsubscribeRate": 0 },
"failedCount": 0,
"failedSubscribers": []
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid period. Must be one of: 1h, 24h, 7d, 30d, 90d"
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Sequence not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Get Transactional Email Metrics
Source: https://docs.sequenzy.com/api-reference/analytics/transactional-metrics
GET /api/v1/metrics/transactional/{idOrSlug}
Get aggregate metrics, clicked links, bounce causes, complaints, replies, and human-versus-machine engagement for one saved transactional email.
Returns aggregate metrics for one saved transactional email selected by its ID
or API slug. Results are all-time by default. Open and click metrics exclude
detected machine engagement unless `includeMachineEngagement=true` is used.
The explicit `engagementBreakdown` always returns both human and machine counts.
Bounce types use the latest bounce event for each delivery, so permanent,
transient, and undetermined counts are mutually exclusive.
Direct-content Send API messages do not have a stable template identity. Use
account metrics with `emailType=transactional`, then search recent deliveries
through `GET /api/v1/email-sends` and inspect one by ID.
`GET /api/v1/transactional/{idOrSlug}/stats` is a compatibility alias.
## How a period is applied
When you filter with `period` or `start`/`end`, the response is a funnel over
the sends made inside that window, not a log of events that happened inside it.
`sends` counts those sends, and every delivery, engagement, and reply count is
attributed to one of them - including a reply or an open that arrives after the
window closes. Activity on sends that predate the window is excluded, so
`opens <= deliveries <= sends` always holds and no rate can exceed 100%.
`opens` and `clicks` are unique counts deduplicated by email send: one recipient
opening the same email five times counts once.
## Parameters
Saved transactional email ID or API slug.
Optional sliding window: `1h`, `24h`, `7d`, `30d`, or `90d`.
ISO 8601 custom range start. Must be used with `end`.
ISO 8601 custom range end. Must be used with `start`; maximum 90 days.
Include detected scanners, previews, and tracked asset engagement.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/transactional/password-reset?period=30d" \
-H "Authorization: Bearer API_KEY"
```
```json 200 theme={null}
{
"success": true,
"period": "30d",
"transactional": {
"id": "transactional_123",
"name": "Password reset",
"slug": "password-reset"
},
"stats": {
"sent": 100,
"delivered": 98,
"bounced": 2,
"opened": 72,
"clicked": 45,
"replies": 6,
"complaints": 1,
"deliveryRate": 98,
"bounceRate": 2,
"openRate": 73.47,
"clickRate": 45.92,
"replyRate": 6.12,
"complaintRate": 1
},
"complaints": { "count": 1, "rate": 1 },
"clickedLinks": [
{
"url": "https://example.com/reset",
"clicks": 40,
"percentage": 88.89
}
],
"bounceBreakdown": {
"permanent": 1,
"transient": 1,
"undetermined": 0,
"subtypes": [
{ "type": "Permanent", "subType": "General", "count": 1 },
{ "type": "Transient", "subType": "MailboxFull", "count": 1 }
]
},
"engagementBreakdown": {
"human": { "opens": 72, "clicks": 45 },
"machine": { "opens": 8, "clicks": 2 }
}
}
```
# Create API Key
Source: https://docs.sequenzy.com/api-reference/api-keys/create
POST /api/v1/api-keys
Create a company-scoped API key
Create a new company-scoped API key. The caller must have the `api_keys:manage` permission. The plain key is returned only once, in this response.
When the caller is an account-scoped `seq_user_` key, select the company with
the `x-company-id` header. The endpoint always creates the new key in the
authenticated request context; a `companyId` field in the JSON body is not a
supported selector.
## Request
Human-readable key name. If omitted, Sequenzy creates a default integration
name.
Permission preset to apply when `scopes` is omitted. Defaults to
`full_access`. Supported presets: `full_access`, `read_only`, `agent_safe`,
`ai_drafting`, `data_ingest_safe`, `data_ingest_automations`,
`transactional_sender`, `marketing_sender`. Full-access keys return a null
`scopes` value, which means all current and future permissions.
Explicit permission scopes for the new key. If provided, this overrides
`preset` and must contain at least one supported scope.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/api-keys" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123" \
-H "Content-Type: application/json" \
-d '{"name": "AI drafting agent", "scopes": ["account:read", "subscribers:read", "emails:write"]}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"apiKey": {
"id": "key_abc123",
"name": "AI drafting agent",
"key": "seq_live_...",
"prefix": "seq_live_abcd",
"scopes": ["account:read", "subscribers:read", "emails:write"],
"permissions": {
"preset": "custom",
"fullAccess": false,
"selectedScopeCount": 3,
"currentScopeCount": 63,
"description": "3 of 63 current permissions are enabled."
},
"createdAt": "2026-05-01T10:30:00Z"
},
"message": "API key created successfully. Save this key securely - it cannot be retrieved later.",
"instructions": {
"envVariable": "SEQUENZY_API_KEY",
"example": "SEQUENZY_API_KEY=seq_live_...",
"note": "Add this to your .env file for the integration to work."
}
}
```
`apiKey.permissions` is the effective permission receipt for the stored key. For
full-access keys, `apiKey.scopes` is `null`, `permissions.preset` is
`full_access`, and `permissions.fullAccess` is `true`. If you explicitly send
every currently defined scope, the key remains a fixed `custom` selection and
does not automatically receive future permissions.
```json 400 theme={null}
{
"error": "Unsupported API key permission preset"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: api_keys:manage"
}
```
```json 500 theme={null}
{
"error": "Failed to create API key"
}
```
# Request Key Handoff
Source: https://docs.sequenzy.com/api-reference/api-keys/handoff
POST /api/v1/api-key-handoff
Get an owner-confirmed dashboard link for creating or rotating an API key
Build a link that opens the dashboard's create-key form with a name and
permissions already filled in. Use this when key management is blocked because
the calling key does not have `api_keys:manage`.
Every other `/api/v1/api-keys` route requires `api_keys:manage`, and that scope
cannot be granted through the API by a key that is missing it - otherwise any
leaked key could mint a full-access successor for itself. This endpoint needs
only `account:read` because it creates nothing, changes nothing, and returns no
secret. The new key is issued in the browser, under the owner's own
authenticated session, after they review the form and click Create.
When the caller is an account-scoped `seq_user_` key, select the company with
the `x-company-id` header. Personal keys are routed to Account → API Keys;
company keys to workspace Settings → API Keys.
## Request
Suggested name for the new key. Trimmed to 80 characters in the link.
Suggested permission preset. Supported presets: `full_access`, `read_only`,
`agent_safe`, `ai_drafting`, `data_ingest_safe`, `data_ingest_automations`,
`transactional_sender`, `marketing_sender`.
Suggested explicit permission scopes. Overrides `preset`. An unsupported scope
is rejected here rather than silently dropped from the owner's form.
ID of the key this one replaces. Pass the literal string `current` for the key
the request is authenticated with. The dashboard offers to revoke the
predecessor once the replacement exists, which finishes a rotation in a single
visit.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/api-key-handoff" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123" \
-H "Content-Type: application/json" \
-d '{"name": "Production sync (rotated)", "preset": "agent_safe", "replaceApiKeyId": "current"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"handoff": {
"url": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys&action=create-api-key&keyName=Production+sync+%28rotated%29&keyPreset=agent_safe&replaces=key_abc123",
"manageUrl": "https://sequenzy.com/dashboard/company/company_abc123/settings?tab=api-keys",
"keyType": "company",
"name": "Production sync (rotated)",
"preset": "agent_safe",
"scopes": null,
"permissions": {
"preset": "agent_safe",
"fullAccess": false,
"selectedScopeCount": 24,
"currentScopeCount": 63,
"description": "24 of 63 current permissions are enabled."
},
"replaces": {
"id": "key_abc123",
"name": "Production sync",
"prefix": "seq_live_abcd",
"isCurrentKey": true
},
"canSelfServe": false,
"deliversKeyToCaller": false
},
"message": "Give this link to the workspace owner. It opens the dashboard create-key form with the requested name and permissions prefilled. The new key is shown in the browser and is never returned here.",
"nextSteps": [
"Send the URL to the workspace owner and stop. Nothing is created until they review the form and click Create.",
"The owner copies the new key from the browser and puts it in your secret store. It is shown once and is never returned through the API.",
"Verify the replacement by calling the account endpoint with the new key before retiring anything.",
"Revoke the previous key: the dashboard offers it after the replacement is created, or the replacement can revoke it through the API when the owner grants it api_keys:manage."
]
}
```
`deliversKeyToCaller` is always `false`. `canSelfServe` is `true` when the
calling key already holds `api_keys:manage` and could create the replacement
directly with [Create API Key](/api-reference/api-keys/create); the link is then
only needed when policy requires a human to confirm.
`permissions` is the receipt for the suggested selection, and `scopes` is the
expanded list. Both are `null` when the request suggested no preset and no
scopes, because the form then opens on the dashboard's own default rather than
on full access.
`replaces.name` and `replaces.prefix` are populated only when the replaced key
is the one making the request. Reading metadata for any other key requires
`api_keys:manage`, so the dashboard resolves it under the owner's session.
```json 400 theme={null}
{
"error": "Unsupported API key scope: not:a:scope"
}
```
```json 400 theme={null}
{
"error": "replaceApiKeyId is not a valid API key ID"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: account:read"
}
```
## Rotating a key you cannot manage
1. Call this endpoint with `replaceApiKeyId: "current"` and the permissions the
replacement needs.
2. Hand the URL to the workspace owner and stop. Do not poll: the key never
comes back through the API.
3. The owner reviews the prefilled form, creates the key, and stores it in your
secret store.
4. Verify the replacement with [Get Account](/api-reference/account/get) before
retiring anything.
5. Revoke the predecessor from the dashboard prompt, or with
[Revoke API Key](/api-reference/api-keys/revoke) if the owner granted the new
key `api_keys:manage`.
# List API Keys
Source: https://docs.sequenzy.com/api-reference/api-keys/list
GET /api/v1/api-keys
List company API keys without exposing secret values
List company-scoped API keys for the selected company. The caller must have the
`api_keys:manage` permission.
The response contains only identification and permission metadata. It never
contains a plain API key, stored key hash, or another recoverable secret.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/api-keys" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123"
```
## Response
```json 200 theme={null}
{
"success": true,
"apiKeys": [
{
"id": "key_abc123",
"name": "SST production handoff",
"prefix": "seq_live_abcd",
"type": "company",
"scopes": ["account:read", "subscribers:read"],
"permissions": {
"preset": "custom",
"fullAccess": false,
"selectedScopeCount": 2,
"currentScopeCount": 63,
"description": "2 of 63 current permissions are enabled."
},
"deviceName": null,
"lastUsedAt": null,
"createdAt": "2026-07-17T10:30:00Z",
"updatedAt": "2026-07-17T10:30:00Z",
"isCurrent": false
}
]
}
```
Use `id`, `name`, `prefix`, and `isCurrent` to verify the exact credential
before revoking it.
```json 403 theme={null}
{
"error": "API key is missing required scope: api_keys:manage"
}
```
# Revoke API Key
Source: https://docs.sequenzy.com/api-reference/api-keys/revoke
DELETE /api/v1/api-keys/{apiKeyId}
Permanently revoke a company API key
Permanently revoke a company-scoped API key. The caller must have the
`api_keys:manage` permission.
Call [List API Keys](/api-reference/api-keys/list) first and compare the key ID,
name, non-secret prefix, and `isCurrent` flag. Revocation cannot be undone, and
revoking the active credential will prevent it from making another request.
Exact API key ID returned by the list API keys endpoint.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/api-keys/key_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123"
```
## Response
```json 200 theme={null}
{
"success": true,
"apiKey": {
"id": "key_abc123",
"name": "SST production handoff",
"prefix": "seq_live_abcd",
"type": "company",
"scopes": ["account:read", "subscribers:read"],
"permissions": {
"preset": "custom",
"fullAccess": false,
"selectedScopeCount": 2,
"currentScopeCount": 63,
"description": "2 of 63 current permissions are enabled."
},
"deviceName": null,
"lastUsedAt": null,
"createdAt": "2026-07-17T10:30:00Z",
"updatedAt": "2026-07-17T10:30:00Z",
"isCurrent": false
},
"message": "API key revoked successfully."
}
```
The response confirms the removed key with metadata only. It never includes the
plain key or stored hash.
```json 404 theme={null}
{
"error": "API key not found"
}
```
# Update API Key
Source: https://docs.sequenzy.com/api-reference/api-keys/update
PATCH /api/v1/api-keys/{apiKeyId}
Rename a company API key or change its permissions in place
Rename a company-scoped API key, replace its permissions, or both. The caller
must have the `api_keys:manage` permission.
The key value does not change. Added permissions apply on the next retry, so use
this instead of issuing a replacement key when a call fails with a missing-scope
error. Removed permissions may remain usable for up to five minutes while API
caches expire.
`preset` and `scopes` replace the whole permission selection rather than
merging into it. Call [List API Keys](/api-reference/api-keys/list) first and
send the full set you want.
## Request
Exact API key ID returned by the list API keys endpoint.
New human-readable key name.
Replacement permission preset: `full_access`, `read_only`, `agent_safe`,
`ai_drafting`, `data_ingest_safe`, `data_ingest_automations`,
`transactional_sender`, or `marketing_sender`.
Replacement explicit permission scopes. Overrides `preset` when provided.
Provide at least one of `name`, `preset`, or `scopes`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/api-keys/key_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "x-company-id: company_abc123" \
-H "Content-Type: application/json" \
-d '{"preset": "agent_safe"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"apiKey": {
"id": "key_abc123",
"name": "Agent key",
"prefix": "seq_live_abcd",
"type": "company",
"scopes": ["account:read", "subscribers:read", "subscribers:tag"],
"permissions": {
"preset": "custom",
"fullAccess": false,
"selectedScopeCount": 3,
"currentScopeCount": 65,
"description": "3 of 65 current permissions are enabled."
},
"deviceName": null,
"lastUsedAt": "2026-08-04T09:12:00Z",
"createdAt": "2026-07-17T10:30:00Z",
"updatedAt": "2026-08-04T10:30:00Z",
"isCurrent": false
},
"message": "API key permissions updated. Added permissions apply on the next retry; removed permissions may take up to five minutes to expire across API instances."
}
```
The response returns metadata only. It never includes the plain key or stored
hash.
```json 400 theme={null}
{
"error": "Provide a name or permissions to update"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: api_keys:manage"
}
```
```json 404 theme={null}
{
"error": "API key not found"
}
```
# List Ad Accounts
Source: https://docs.sequenzy.com/api-reference/audience-syncs/ad-accounts
GET /api/v1/ad-accounts
List the Meta ad accounts available for audience syncs
List the Meta ad accounts reachable through your connected Meta Ads integration. Use the returned `id` as `adAccountId` when [creating an audience sync](/api-reference/audience-syncs/create).
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/ad-accounts" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"adAccounts": [
{
"id": "act_1234567890",
"name": "Acme Main Account"
}
]
}
```
```json 400 theme={null}
{
"error": "Meta Ads is not connected for this company. Connect it in Settings -> Integrations."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# Create Audience Sync
Source: https://docs.sequenzy.com/api-reference/audience-syncs/create
POST /api/v1/audience-syncs
Push a segment to a Meta custom audience and keep it synced
Create an audience sync. The custom audience is created in your Meta ad account, the first upload runs immediately, and afterwards the audience refreshes on the chosen schedule.
Requires the Meta Ads integration to be connected in the dashboard (Settings → Integrations).
## Request
Existing segment ID to sync. Provide either `segmentId` or
`predefinedSegmentId`, not both.
Ready-made segment template ID, for example `zero-ltv`, `no-purchase-1y`,
`recent-buyers`, `high-spenders-ecom`, `non-buyers`, or `engaged`. The segment
is created automatically on first use and reused afterwards. If an unrelated
segment with the template's name but different filters already exists, the
request fails with a `409` - sync that segment via `segmentId` or rename it.
Meta ad account ID (`act_...`). Use [List Ad
Accounts](/api-reference/audience-syncs/ad-accounts) to find it.
Name for the custom audience in Meta Ads Manager.
How often the segment is re-uploaded: `hourly`, `daily`, or `weekly`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/audience-syncs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"predefinedSegmentId": "recent-buyers",
"adAccountId": "act_1234567890",
"audienceName": "Sequenzy - Recent buyers",
"frequency": "daily"
}'
```
Audiences are add-only: subscribers who later leave the segment stay in the
Meta audience. Meta requires 100+ matched people before an audience can be
used for ad delivery.
## Responses
```json 200 theme={null}
{
"success": true,
"audienceSync": {
"id": "sync_abc123",
"segmentId": "seg_recent_buyers",
"segmentName": "Recent buyers",
"adAccountId": "act_1234567890",
"audienceName": "Sequenzy - Recent buyers",
"frequency": "daily",
"isActive": true,
"syncStatus": "idle",
"lastSyncAt": null,
"lastSyncError": null,
"lastSyncedCount": 0,
"createdAt": "2026-06-12T10:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "Meta Ads is not connected for this company. Connect it in Settings -> Integrations."
}
```
```json 400 theme={null}
{
"error": "Provide either a segmentId or a predefinedSegmentId, not both"
}
```
```json 404 theme={null}
{
"error": "Segment not found"
}
```
```json 409 theme={null}
{
"error": "This segment is already synced to an audience in that ad account"
}
```
```json 409 theme={null}
{
"error": "A segment named \"Recent buyers\" already exists with different filters. Sync it via segmentId, or rename it to use the \"recent-buyers\" template."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# Delete Audience Sync
Source: https://docs.sequenzy.com/api-reference/audience-syncs/delete
DELETE /api/v1/audience-syncs/{syncId}
Remove an audience sync mapping
Remove an audience sync. The Meta audience itself is kept - running ads referencing it are not disrupted. Only future syncs stop.
## Request
Audience sync ID
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/audience-syncs/sync_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 404 theme={null}
{
"error": "Audience sync not found"
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# List Audience Syncs
Source: https://docs.sequenzy.com/api-reference/audience-syncs/list
GET /api/v1/audience-syncs
List segment-to-Meta-audience syncs with schedule and status
List your audience syncs - mappings that push a segment's subscribers to a Meta custom audience on a schedule.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/audience-syncs" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"audienceSyncs": [
{
"id": "sync_abc123",
"segmentId": "seg_recent_buyers",
"segmentName": "Recent buyers",
"adAccountId": "act_1234567890",
"audienceName": "Sequenzy - Recent buyers",
"frequency": "daily",
"isActive": true,
"syncStatus": "idle",
"lastSyncAt": "2026-06-12T10:00:00.000Z",
"lastSyncError": null,
"lastSyncedCount": 4216,
"createdAt": "2026-06-01T09:00:00.000Z"
}
]
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# Run Audience Sync
Source: https://docs.sequenzy.com/api-reference/audience-syncs/sync
POST /api/v1/audience-syncs/{syncId}/sync
Trigger an immediate sync run outside the schedule
Trigger an immediate upload of the segment's active subscribers to the Meta audience, outside the regular schedule. The sync must be active (not paused).
## Request
Audience sync ID
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/audience-syncs/sync_abc123/sync" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"queued": true
}
```
```json 400 theme={null}
{
"error": "Resume the sync before running it"
}
```
```json 404 theme={null}
{
"error": "Audience sync not found"
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# Update Audience Sync
Source: https://docs.sequenzy.com/api-reference/audience-syncs/update
PATCH /api/v1/audience-syncs/{syncId}
Change an audience sync's frequency or pause/resume it
Update an audience sync's schedule or pause/resume it.
## Request
Audience sync ID
New sync frequency: `hourly`, `daily`, or `weekly`.
Set `false` to pause the sync, `true` to resume it.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/audience-syncs/sync_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"frequency": "weekly", "isActive": false}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"audienceSync": {
"id": "sync_abc123",
"segmentId": "seg_recent_buyers",
"segmentName": "Recent buyers",
"adAccountId": "act_1234567890",
"audienceName": "Sequenzy - Recent buyers",
"frequency": "weekly",
"isActive": false,
"syncStatus": "idle",
"lastSyncAt": "2026-06-12T10:00:00.000Z",
"lastSyncError": null,
"lastSyncedCount": 4216,
"createdAt": "2026-06-01T09:00:00.000Z"
}
}
```
```json 404 theme={null}
{
"error": "Audience sync not found"
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
# Get Campaign Audience
Source: https://docs.sequenzy.com/api-reference/campaigns/audience
GET /api/v1/campaigns/{campaignId}/audience
Resolve who a campaign will reach, with a live recipient count
Resolve a campaign's stored targeting into named lists and segments plus a recipient count computed at read time.
Use this to verify a scheduled campaign's audience before it sends. Unlike `targetLists` on [Get Campaign](/api-reference/campaigns/get), which contains raw IDs, this endpoint returns resolved names, flags references to deleted records, and reports whether targeting was ever set.
When `audience.isUnset` is `true`, the campaign has no targeting. Scheduling it
sends to every active subscriber, and `recipientCount` reflects that fallback.
## Request
Campaign ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/audience" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
One of `unset`, `all`, `lists`, `segment`, `filtered`, or `rules`.
Plain-language description of who the campaign reaches.
True when targeting has never been set.
Resolved lists. Each entry has `id`, `name`, and `missing` - `missing` is true
when the referenced list no longer exists.
Resolved segments, in the same shape as `lists`.
Include rules for `rules` targeting, with lists and segments resolved.
Exclude rules for `rules` targeting, with lists and segments resolved.
Subscribers matching the effective targeting right now.
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "camp_abc123",
"campaignName": "April Launch",
"status": "scheduled",
"scheduledAt": "2026-05-02T09:00:00Z",
"audience": {
"type": "lists",
"summary": "Subscribers on lists Newsletter. 1284 recipient(s) match right now.",
"isUnset": false,
"lists": [{ "id": "list_123", "name": "Newsletter", "missing": false }],
"segments": [],
"filters": [],
"filterJoinOperator": null,
"include": [],
"exclude": [],
"excludedSubscriberIds": [],
"includedSubscriberIds": [],
"excludedCampaignOpenerIds": []
},
"recipientCount": 1284,
"targetLists": { "type": "lists", "listIds": ["list_123"] }
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: campaigns:read"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
# Cancel Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/cancel
POST /api/v1/campaigns/{campaignId}/cancel
Cancel a scheduled or sending campaign
Cancel a campaign in `sending`, `paused`, `scheduled`, `waiting_approval`, or `rejected` status. Pending send jobs are removed, and any remaining recipients are not emailed. Cancelled campaigns cannot be resumed. To edit and schedule a scheduled campaign again, [unschedule it](/api-reference/campaigns/unschedule) instead.
## Request
Campaign ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/cancel" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "cancelled",
"labels": [],
"scheduledAt": "2026-06-01T14:00:00.000Z",
"sentAt": null,
"createdAt": "2026-05-20T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"error": "Cannot cancel a campaign in \"sent\" status."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 409 theme={null}
{
"error": "Campaign status has changed and can no longer be cancelled."
}
```
# Check Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/check
POST /api/v1/campaigns/{campaignId}/check
Find broken links, placeholders and content problems before sending a campaign
Run the pre-send email check on a saved campaign: the same rules as the email checker in the editor, plus live verification of every link and image. Use it in your own approval flow, or before scheduling a campaign through the API.
This endpoint is read-only. It requires the `campaigns:read` scope and never
sends, schedules, or modifies anything. It uses `POST` only so personalization
input can travel in a request body.
## Request
Campaign ID.
Check as this stored subscriber: picks their localization and reports merge
tags and conditions that would not resolve for them. Link and content rules
always run on the email as written. Use this or `subscriber`, not both.
Requires the `subscribers:read` scope as well.
Check as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Extra merge variables layered over the contact's attributes.
Check a specific localization instead of deriving it from the contact.
Check a specific A/B test variant of this campaign.
Verify every link and image over the network. Pass `false` for a fast,
rules-only check that makes no requests.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/check" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
## What gets checked
Links and images are collected from every version recipients can receive: each A/B variant and each translation. Pass `variantId` or `locale` to check only that version.
**Links and images, live.** Every web link and image is requested from our servers (HEAD, then GET when a server refuses HEAD), following up to 8 redirects. Each one comes back with a `status`:
| Status | Meaning |
| - | - |
| `ok` | Answered with a success status, after any redirects. |
| `broken` | Page not found (404/410), the domain doesn't exist, an SSL certificate problem, a redirect loop, or a rejected request. Images that don't return an image also count. |
| `invalid` | Can never work for recipients: a rule already found it unusable (for example localhost, a private address, a mistyped `https://` or no `https://` at all), or the domain points to a private address. |
| `server_error` | The server answered with a 5xx error. Often temporary. |
| `unreachable` | No answer in time, or the connection was refused or reset. |
| `restricted` | The site refused an automated check (for example 401, 403 or 429), or redirects to itself to set a cookie. It may work fine in a browser, so it is never counted as broken. |
| `personalized` | Built from merge tags filled in per recipient, such as `{{event.orderUrl}}`. Only its syntax is checked. |
| `not_checked` | `mailto:` and `tel:` links, links not reached before the 25-second time limit, or `links` was `false`. |
`{{company.url}}` is resolved from your company website before checking. If no website is set, those links are reported as `links.missing-website`.
**Links, without a request.** Placeholder links (`#`, a bare `https://`), buttons with no link, script links, links missing `https://` or relative links, mistyped schemes such as `htps://`, unencoded spaces, incomplete domains, localhost and private addresses, tunnel and deploy-preview URLs, staging subdomains, placeholder domains such as `example.com`, `http://` links, link shorteners, raw IP addresses, link text that shows a different domain than the link goes to, invalid `mailto:`/`tel:` links, and unclosed merge tags in URLs.
**Images.** Broken or non-image URLs, images over 1 MB, embedded base64 images, local file paths, placeholder image services, and missing alt text.
**Content.** Leftover placeholder copy (lorem ipsum, `[First Name]`, TODO), name merge tags without a fallback, spam-trigger wording, excessive capitals, profanity, too many or no links, a missing unsubscribe link in marketing email, empty conditional branches, and broken merge tag syntax.
**Subject and preview text.** Length, placeholder text, a fake `Re:` or `Fwd:` prefix, spam-trigger wording, capitals, punctuation, emoji, and preview text that repeats the subject.
**Rendered HTML.** Gmail clipping risk, button contrast and tap-target size, and features specific email clients drop.
Sender settings (From and Reply-To) are not part of this check.
## Response
`true` when the check ran.
0 to 100 across subject (25%), preview text (15%) and content (40%), rescaled
to 100 because sender settings aren't part of this check. The editor also
weighs your sender (20%), so its score can differ slightly for the same email.
`A`, `B`, `C`, `D` or `F`.
Predicted inbox tab: `Primary`, `Promotions` or `Spam`.
`subject`, `preview` and `content`, each with `score` and `maxScore`.
Findings, errors first. Each has `category`, `severity` (`error` to fix before
sending, `warning` to review, `info` for tips) and `message`, plus a stable
`rule` ID such as `links.broken` or `content.placeholder-text`. When the
finding has one location, `blockId` names the block to edit and `url` the link
or image concerned.
Every link and image: `url`, `kind` (`link` or `image`), `label`, `blockId`,
`blockType`, `status`, `httpStatus`, `finalUrl` (after redirects), `message`,
and `findings` from the rules that need no request.
Counts: `total`, `checked`, `ok`, `broken` (broken and invalid), `warnings`
(server\_error and unreachable), `restricted` and `notChecked` (personalized
and not\_checked).
`false` when you passed `links: false`.
Subject line with merge tags resolved for the checked contact.
Preview text with merge tags resolved, or `null` when unset.
Localization the check used.
Merge tags that did not resolve, as on the [render
endpoint](/api-reference/campaigns/render).
Block conditions this check could not decide, as on the [render
endpoint](/api-reference/campaigns/render).
What was checked: `type`, `id` and `variantId`.
Results for the same URL are cached for up to 10 minutes (1 minute for failures), so repeating a check right after fixing a broken link can briefly return the old answer. Live checks are limited to 30 calls a minute per API key and company, and 60 a minute per API key across companies; past that you get `429`, and `links: false` still works.
```json 200 theme={null}
{
"success": true,
"score": 79,
"grade": "C",
"placement": "Promotions",
"categories": {
"subject": { "score": 100, "maxScore": 100 },
"preview": { "score": 80, "maxScore": 100 },
"content": { "score": 65, "maxScore": 100 }
},
"issues": [
{
"category": "content",
"severity": "error",
"rule": "links.broken",
"message": "Broken link: \"Shop the sale\" (acme.com/sale) - Page not found (404).",
"blockId": "block-hero",
"url": "https://acme.com/sale"
},
{
"category": "content",
"severity": "error",
"rule": "links.missing-scheme",
"message": "Missing https:// - email clients won't open this: \"new arrivals\" (www.acme.com/new).",
"blockId": "block-intro",
"url": "www.acme.com/new"
}
],
"links": [
{
"url": "https://acme.com/sale",
"kind": "link",
"label": "Shop the sale",
"blockId": "block-hero",
"blockType": "hero",
"status": "broken",
"httpStatus": 404,
"finalUrl": null,
"message": "Page not found (404)",
"findings": []
}
],
"linkSummary": {
"total": 6,
"checked": 4,
"ok": 3,
"broken": 1,
"warnings": 0,
"restricted": 0,
"notChecked": 2
},
"linksChecked": true,
"subject": "New arrivals picked for you",
"previewText": "Fresh styles and restocked favorites",
"locale": "en",
"unresolvedMergeTags": [],
"unevaluatedConditions": [],
"entity": { "type": "campaign", "id": "camp_abc123", "variantId": null }
}
```
```json 400 theme={null}
{
"error": "Provide either subscriberId or subscriber, not both."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 429 theme={null}
{
"error": "Too many link checks. Try again in a minute, or pass links: false for a rules-only check."
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
# Create Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/create
POST /api/v1/campaigns
Create an email campaign
If you use an account key as a marketer, choose an existing sender and
Reply-To profile. A request that needs a new profile returns `400` before
creating profiles, labels, or campaign/sequence changes. This also applies to
email steps and inserted sequence branches. An owner or admin can add the
identity first; a step-level display-name override can still use an existing
profile.
Create a campaign and linked email with at most one content source: `prompt` for Sequenzy-generated branded native blocks, `blocks` for finished caller-supplied content, `html` for preserved/imported markup, or `templateId` to copy an existing company template. Omit all four to create an empty draft for later editing. `style` and `tone` are valid only with `prompt`; subject and preview values may override generated values. Use `emailPreset` to set the native email's Style > Format independently of prompt-generation `style`. You may select or create campaign-specific From and Reply-To profiles; otherwise account defaults apply. Campaigns are drafts by default. For migrations, `status: "sent"` archives an already-sent campaign without sending. The response includes `emailId`, the created email body; it appears in [List Templates](/api-reference/templates/list) and can be reused as `templateId` for later campaigns.
## Request
Campaign name.
Email subject line. Required with `html`, `blocks`, or `templateId`; optional
with `prompt`, where it overrides the generated subject.
Natural-language request for Sequenzy to generate branded native campaign
blocks. Use this instead of `html`, `blocks`, or `templateId`.
Optional generation style. Valid only with `prompt`. Pass `designed` or
`plain` to force the designed or plain-text email style; other values are
freeform prompt guidance. Defaults to your company's email style preference
(designed unless you chose plain text).
Optional generation tone. Valid only with `prompt`.
Per-email Style > Format: `branded` or `minimal`. Supported for native
Sequenzy blocks, including supported custom HTML blocks, but not a standalone
raw HTML email; do not combine it with `html`. Applying `minimal` removes
standalone logo blocks. Switching back to `branded` generates a new logo, so
send the authored logo block again if its ID and alt text must be preserved.
Optional inbox preview text saved on the linked email.
Alias for `previewText`. Provide only one of `previewText` or `preheaderText`.
Optional campaign tracking code available to UTM templates as
`{{campaign.trackingCode}}`. Empty strings are stored as `null`.
Initial status. Use `draft` or `sent`; defaults to `draft`. `sent` is for
imported/already-sent campaigns only.
ISO date-time for an imported/already-sent campaign. Only valid with `status:
"sent"`; defaults to the current time when omitted.
Raw HTML body. Sequenzy converts this into email blocks. Use this or `blocks`,
not both.
Sequenzy email blocks. Use this or `html`, not both. Put block styling under
`styles`; top-level style keys like `backgroundColor`, `backgroundOpacity`,
`textColor`, `textAlign`, `borderColor`, `borderWidth`, and `borderRadius` are
normalized into `styles`.
Company-owned email template to copy into the campaign. Use this instead of
`prompt`, `html`, or `blocks`. The content snapshot of a code-managed
transactional email (created by [sends with
`trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code))
is rejected with `400` because it holds one recipient's real content.
Label names to assign to the campaign. Missing labels are created
automatically. The API also accepts `label` as a compatibility alias.
Campaign audience saved on the draft, for example `{"type": "lists",
"listIds": ["list_123"]}` to send straight to a list. Also accepts `{"type":
"all"}`, `{"type": "segment", "segmentId": "seg_123"}`, `{"type": "filtered",
...}`, and `{"type": "rules", ...}`. Omit it to leave targeting unset and set
the audience later with [update](/api-reference/campaigns/update) or
[schedule](/api-reference/campaigns/schedule). Mutually exclusive with
`segmentId` and `listIds`.
Shorthand for targeting one saved segment, equivalent to `{"type": "segment",
"segmentId": "seg_123"}`. Mutually exclusive with `targetLists` and
`listIds`.
Shorthand for targeting one or more lists, equivalent to `{"type": "lists",
"listIds": ["list_123"]}`. Mutually exclusive with `targetLists` and
`segmentId`.
Campaign-scoped JSON data available while rendering this campaign.
Personalized list definitions computed from `campaignData`.
Campaign From address. Its domain must be configured and verified. Mutually
exclusive with `senderProfileId`.
Display name recipients see, e.g. `Brennon at TradeTally`. Selects the sender
identity of that name on `fromEmail`, creating it when the address has no
identity by that name; the mailbox's other display names, and everything
pinned to them, are untouched. Requires `fromEmail`; omit it when you send
`senderProfileId`, which already carries its own display name.
Existing sender profile ID. It already supplies both the From address and the
display name, so send it on its own and omit `fromEmail` and `fromName`.
Campaign Reply-To address. Mutually exclusive with `replyProfileId`.
Display name for the Reply-To address. Requires `replyTo`; omit it when you
send `replyProfileId`, which already carries its own display name. An address
carries one Reply-To name company-wide: if `replyTo` already has a saved
profile under a different name, that saved name is kept and the response
`warnings` array says so.
Existing reply profile ID. It already supplies both the Reply-To address and
the display name, so send it on its own and omit `replyTo` and `replyToName`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "April Launch",
"subject": "A quick update",
"preheaderText": "A short preview for the inbox",
"labels": ["edm", "launch"],
"html": "
"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"emailId": "email_abc123",
"emailPreset": "branded",
"status": "draft",
"trackingCode": null,
"labels": ["edm", "launch"],
"targetLists": { "type": "lists", "listIds": ["list_123"] },
"sentAt": null,
"senderProfileId": "sender_abc123",
"fromName": "Acme",
"fromEmail": "hello@example.com",
"replyProfileId": "reply_abc123",
"replyToName": "Support",
"replyToEmail": "support@example.com",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at most one of prompt, html, blocks, or templateId."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide either previewText or preheaderText, not both."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at most one of segmentId, listIds, or targetLists. Received: segmentId, targetLists."
}
```
```json 400 theme={null}
{
"success": false,
"error": "No sender profile configured. Please set up a sender first."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid campaign status. Create supports: draft, sent."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create campaign"
}
```
# Delete Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/delete
DELETE /api/v1/campaigns/{campaignId}
Permanently delete a campaign
Permanently delete a campaign. Active campaigns in `sending`, `scheduled`, or `paused` status must be cancelled first so pending send jobs are cleaned up. This action cannot be undone.
## Request
Campaign ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 400 theme={null}
{
"error": "Cancel the campaign before deleting it."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
# Duplicate Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/duplicate
POST /api/v1/campaigns/{campaignId}/duplicate
Create a draft copy of a campaign
Create a draft copy of a campaign with a `(Copy)` suffix. You can duplicate the plain campaign, copy the campaign together with its A/B test and variants, or copy a single variant's content as a plain campaign.
## Request
Campaign ID to duplicate.
Duplication mode. `campaign` copies the campaign email (default), `ab_test`
also copies the linked A/B test and variants, `variant` copies one variant's
content as a plain campaign.
Variant ID to copy. Required when `mode` is `variant`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/duplicate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "ab_test"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_new123",
"name": "April Launch (Copy)",
"subject": "A quick update",
"status": "draft",
"labels": [],
"emailId": "email_new123",
"scheduledAt": null,
"sentAt": null,
"createdAt": "2026-06-11T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_new123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_new123?step=review"
}
}
```
```json 400 theme={null}
{
"error": "Variant ID is required when duplicating a single variant"
}
```
```json 400 theme={null}
{
"error": "A/B test variant not found for this campaign"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 404 theme={null}
{
"error": "A/B test not found for this campaign"
}
```
```json 500 theme={null}
{
"error": "Failed to duplicate campaign"
}
```
# Create Campaign for a Group
Source: https://docs.sequenzy.com/api-reference/campaigns/from-audience
POST /api/v1/campaigns/from-audience
Create a draft from selected contacts, contact filters or an email activity group
Create a blank campaign for a group you have already found. Membership is frozen when the draft is created; new contacts do not join it automatically. Active status, a usable email address and other delivery exclusions are checked again before sending. This endpoint creates no sends and needs no sender setup. It saves contact IDs directly on the draft without creating a list, changing list memberships or enrolling contacts in sequences. The dashboard uses the same selection rules when you choose a campaign template for a filtered group; public clients create a blank draft and then update its content.
You need `campaigns:write` and `subscribers:read`. An `email_activity` selection also needs `analytics:read`. Marketers can select marketing sources; viewers and restricted members cannot create drafts.
## Select contacts
```json theme={null}
{
"name": "Customer update",
"selection": {
"source": "contacts",
"contacts": { "subscriberIds": ["sub_1", "sub_2"] }
}
}
```
Omit `subscriberIds` to include **every** matching contact across pages. You can combine `search`, `listId`, `activeOnly` and `excludedSubscriberIds` with either `segmentId`, flat `filters` plus `filterJoinOperator`, or a nested `root`. Do not combine a saved segment with edited filters, or `root` with `filters`. Filters use the same fields and operators as [subscriber filtering](/concepts/subscribers#filtering-subscribers). Search follows contact search semantics. With no filters, the selection includes every contact in the company; email eligibility is reported separately.
## Select an activity group
```json theme={null}
{
"selection": {
"source": "email_activity",
"activity": {
"campaignId": "camp_original",
"eventType": "click",
"period": "7d",
"includeMachineEngagement": false,
"mailboxProvider": "google"
}
}
}
```
Choose one of `campaignId`, `automationId`, `automationNodeId` or `transactionalId`, or an `audience` of `{ "type": "list" | "segment", "id": "..." }`. An audience can also narrow one email source. Supported events are `send`, `delivery`, `open`, `click`, `bounce` and `unsubscribe`. Periods are `all`, `1h`, `24h`, `7d`, `30d` and `90d`; omitting the period selects all time. The selection also supports `search`, `includeMachineEngagement`, `mailboxProvider`, and bounce-only `bounceType` and `bounceSubType`.
Whole-sequence activity includes regular email steps and A/B email variants. Each contact appears once even if they have multiple matching events. Deleted contacts in historical analytics are omitted. The dashboard additionally honors your global machine-engagement preference; API clients set `includeMachineEngagement` explicitly.
## Response and next steps
`201` returns `campaign`, including the linked blank `email`, and `audience` with `source`, `selectedCount`, `eligibleCount` and `selectedAt`. The saved `targetLists` uses `{ "type": "rules", "include": [], "includedSubscriberIds": [...] }`.
Use [Get Campaign](/api-reference/campaigns/get) or [Get Campaign Audience](/api-reference/campaigns/audience) to review it. [Update Campaign](/api-reference/campaigns/update) edits content, replaces `targetLists`, or clears it with `null`. Clearing removes the saved selection; review the resulting audience before [scheduling](/api-reference/campaigns/schedule). Selected IDs satisfy the include requirement even with an empty `include` array.
Selections are limited to 100,000 matching contacts and 8 MiB of stored audience JSON. Activity selections apply the contact-count limit before removing deleted contacts. Oversized selections are rejected instead of truncated. Use a saved segment for a larger audience.
## Errors
| Status | Meaning |
| - | - |
| `400` | Invalid, ambiguous or incompatible selection, oversized explicit ID arrays, or no matching contacts. |
| `401` | Missing or invalid API key. |
| `403` | Missing scopes or role permissions. Newly granted scopes are refreshed on retry. |
| `404` | A selected contact or source is missing or outside your company. |
| `413` | Resolved contact count or stored audience size exceeds the limit. |
Errors return `{ "error": "..." }`. Draft creation is atomic: a failed campaign insert does not leave an orphan email. Repeating a successful creation makes another draft; inspect your campaign list if a response is lost.
Explicit included IDs override exclusions. To remove a contact from a frozen group, remove its ID from `includedSubscriberIds`. Creation-time `selection.contacts.excludedSubscriberIds` removes contacts before the group is frozen.
# Get Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/get
GET /api/v1/campaigns/{campaignId}
Get one campaign with its email content
Get a campaign and its editable email blocks.
`emailId` is the campaign's linked email body - the same record returned by the
[templates API](/api-reference/templates/get) - so you can correlate a campaign
with its entry in [List Templates](/api-reference/templates/list), or pass it as
`templateId` to [Create Campaign](/api-reference/campaigns/create) to reuse the
design. It is `null` for SMS campaigns.
`emailPreset` is that email's Style > Format: `branded` (company logo and full
footer) or `minimal` (no logo, simple footer). It is derived from the stored
blocks and reported the same way for
[sequence steps](/api-reference/sequences/get) and
[transactional templates](/api-reference/transactional/get), so you can check
that a campaign shares its chrome with the rest of your emails without
rendering them. It is `null` for SMS campaigns and for an email stored as a
single raw HTML block, which has no Sequenzy format.
`status` is one of `draft`, `scheduled`, `waiting_approval`, `rejected`,
`sending`, `paused`, `sent`, or `cancelled`. A campaign in `waiting_approval` is
held for [safety review](/concepts/campaigns#safety-review) -
common on new accounts - and is not sending. `rejectionComment` carries reviewer
feedback once a held campaign is `rejected`; it stays `null` while the campaign
is still waiting.
For native sends, `sentAt` is stamped after the last recipient is handed off,
so for a paced send it is the end of the delivery window rather than when
sending started. The pacing itself comes back alongside it: `spreadOverHours`
(null when there is no fixed spread), `sendTimeOptimization`, and
`sendTimeWindowHours`. When the
spread is null, inspect `sendTimeOptimization` to distinguish an optimal-time
send from a campaign with no recorded pacing. Imported campaigns may not carry
delivery-strategy metadata from their source provider. For native campaigns,
`spreadOverHours` and `sendTimeOptimization` mirror what you pass to
[Schedule Campaign](/api-reference/campaigns/schedule), while
`sendTimeWindowHours` reports the stored optimization window. `maxRecipients`
is the recipient cap applied when the audience is resolved at send time, or
`null` when the whole audience is targeted.
## Request
Campaign ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"emailId": "email_abc123",
"preheader": "Latest product news",
"preheaderText": "Latest product news",
"status": "draft",
"rejectionComment": null,
"labels": ["edm", "launch"],
"blocks": [],
"emailPreset": "branded",
"campaignData": null,
"computedLists": [],
"targetLists": { "type": "lists", "listIds": ["list_123"] },
"senderProfileId": "sender_abc123",
"fromName": "Acme",
"fromEmail": "hello@example.com",
"replyProfileId": "reply_abc123",
"replyToName": "Support",
"replyToEmail": "support@example.com",
"scheduledAt": null,
"sentAt": null,
"spreadOverHours": null,
"sendTimeOptimization": false,
"sendTimeWindowHours": 12,
"maxRecipients": null,
"createdAt": "2026-05-01T10:30:00Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
# Campaign Goals
Source: https://docs.sequenzy.com/api-reference/campaigns/goals
Create and manage persisted conversion goals for a campaign
Campaign goals are the persisted conversion signals shown on a campaign
report. A conversion is attributed when the subscriber was sent the campaign
within the goal's attribution window. When engagement exists, the most recent
click is preferred over the most recent open as the stronger last-touch signal.
Goals appear as named steps after sent, opened, and clicked.
Campaign goals currently support email campaigns only. SMS campaign IDs are
rejected because SMS delivery and attribution use a separate event pipeline.
## Endpoints
* `GET /api/v1/campaigns/{campaignId}/goals` lists goals.
* `POST /api/v1/campaigns/{campaignId}/goals` creates a goal.
* `PATCH /api/v1/campaigns/{campaignId}/goals/{goalId}` updates a goal.
* `DELETE /api/v1/campaigns/{campaignId}/goals/{goalId}` deletes a goal.
Event goals use `triggerType: "event"` and require `triggerEventName`.
Subscriber-attribute goals use `triggerType: "attribute_change"`, an
`attributePath`, and `attributeCondition` set to `changed`, `changed_to`, or
`changed_from_to`. The latter conditions also require the corresponding target
and previous values. Tag goals use `triggerType: "tag_added"` and require
`triggerTagName`.
```json theme={null}
{
"name": "Recipient signed up",
"triggerType": "event",
"triggerEventName": "custom.signup",
"attributionWindowHours": 168
}
```
For event goals, `eventPropertyName` and `eventPropertyLabel` can track a
numeric property such as revenue. `attributionWindowHours` accepts whole hours
from 1 to 720 and defaults to 168 hours for campaign goals. Both create and
update accept `isActive`, so you can create a goal disabled or pause tracking
later without deleting the goal.
Campaign goal names are unique within each campaign. The same name may be used
by another campaign or by a company-wide or sequence goal. Creating or renaming
a campaign goal to a duplicate name returns `409 Conflict`.
The dashboard attaches these from the campaign **⋯** menu. The CLI exposes
the same operations under `sequenzy campaigns goals`, and MCP clients can use
`list_campaign_goals`, `create_campaign_goal`, `update_campaign_goal`, and
`delete_campaign_goal`.
## Company-wide goals from Settings
`GET /api/v1/campaigns/{campaignId}/goals` returns two arrays:
* `goals`: Goals created specifically for this campaign. Existing update and delete routes apply to these IDs.
* `companyGoals`: Company-wide goals created in Settings. They apply automatically across emails, including this campaign, so you do not select or copy them. They retain `scope: "company"`, `campaignId: null`, their trigger, attribution window, and active state. Both arrays include inactive goals and are empty when no matching goals exist.
Company-wide custom goals credit an email after a qualifying open or click within their configured window. Campaign-specific goals can also credit a send without an open or click. Creating a campaign goal with the same trigger creates separate tracking; it does not attach the Settings goal. Manage company-wide goals in Settings.
The dashboard lists company-wide goals separately, and `sequenzy campaigns goals` and `list_campaign_goals` expose the same definitions. Restricted members cannot read company-wide Settings goals. Use the [campaign metrics endpoint](/api-reference/analytics/campaign-metrics) to inspect attributed campaign results.
Reads require a valid API key and existing campaign read permissions. Missing or invalid authentication returns `401`; insufficient scopes or role returns `403`; unknown or other-company campaigns return `404`; SMS campaigns return `400`. Transient database failures can return `503`. Retry a failed read; listing goals has no write side effects.
# List Campaigns
Source: https://docs.sequenzy.com/api-reference/campaigns/list
GET /api/v1/campaigns
List email campaigns
List campaigns for the authenticated company. You can filter by status or label.
Results are ordered newest first. The default page size is 50, and `limit` is
capped at 100. Each item includes delivery pacing
(`sendTimeOptimization`, `sendTimeWindowHours`, `spreadOverHours`,
`sendInRecipientTimezone`, `scheduledTimezone`) and the recipient cap
(`maxRecipients`, `null` when the whole audience is targeted) so a company-wide
Send Time Optimization audit does not need one Get Campaign call each. STO is
campaign-only; sequences use `sendingWindow`.
Each item also includes `hasAudience`. It is true when an explicit audience is
configured, including a saved list, segment, filters, selected contacts or an
explicit all-contacts choice. It is false for unset or empty selections. It does
not count eligible recipients or validate that referenced audiences still exist.
Use it to show an **Audience set** indicator on drafts, and review the audience
before sending.
## Request
Optional status filter: `draft`, `scheduled`, `sent`, `sending`, `cancelled`,
`paused`, `waiting_approval`, or `rejected`.
Optional label name filter. Only campaigns assigned this label are returned.
Optional page size. Defaults to `50`; maximum `100`.
Optional zero-based row offset. Defaults to `0`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/campaigns?status=draft&label=edm&limit=100&offset=0" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaigns": [
{
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"emailId": "email_abc123",
"emailPreset": "branded",
"status": "draft",
"hasAudience": true,
"rejectionComment": null,
"labels": ["edm"],
"scheduledAt": null,
"sentAt": null,
"spreadOverHours": null,
"sendTimeOptimization": false,
"sendTimeWindowHours": 12,
"sendInRecipientTimezone": false,
"scheduledTimezone": null,
"maxRecipients": null,
"createdAt": "2026-05-01T10:30:00Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
],
"pagination": {
"limit": 100,
"offset": 0,
"count": 1,
"total": 1,
"hasMore": false
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid status filter. Valid values: draft, scheduled, sent, sending, cancelled, paused, waiting_approval, rejected"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
`emailPreset` is the linked email's Style > Format: `branded` (company logo
and full footer), `minimal` (no logo, simple footer), or `null` for SMS
campaigns and emails stored as a single raw HTML block. Use it to check chrome
consistency across campaigns without fetching each one.
# Pause Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/pause
POST /api/v1/campaigns/{campaignId}/pause
Pause a campaign that is currently sending
Pause a campaign in `sending` status. In-progress send workers stop, and remaining recipients are held until you resume or cancel the campaign.
## Request
Campaign ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/pause" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "paused",
"labels": [],
"scheduledAt": "2026-06-01T14:00:00.000Z",
"sentAt": null,
"createdAt": "2026-05-20T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"error": "Cannot pause a campaign in \"draft\" status."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 409 theme={null}
{
"error": "Campaign status has changed and can no longer be paused."
}
```
```json 500 theme={null}
{
"error": "Failed to pause campaign. Please try again."
}
```
# Preview Campaign Personalized Lists
Source: https://docs.sequenzy.com/api-reference/campaigns/preview-computed-data
POST /api/v1/campaigns/{campaignId}/preview-computed-data
Preview campaign personalized lists for a subscriber
Preview the per-recipient lists that a campaign will compute from its campaign data.
## Request
Campaign ID.
Existing subscriber ID to use for the preview. Use this or `subscriber`.
Inline preview subscriber. Use this or `subscriberId`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/preview-computed-data" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subscriber": {
"email": "anna@example.com",
"customAttributes": {
"region": "Auckland",
"interests": ["theatre", "arts"]
}
}
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"variables": {
"campaign": {
"events": []
},
"recommendedEvents": [
{
"id": "evt_123",
"title": "Romeo and Juliet"
}
]
},
"lists": [
{
"key": "recommendedEvents",
"items": [
{
"id": "evt_123",
"title": "Romeo and Juliet"
}
],
"exposures": [
{
"slot": 1,
"id": "evt_123",
"title": "Romeo and Juliet"
}
]
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide subscriberId or subscriber."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
# Render Campaign HTML
Source: https://docs.sequenzy.com/api-reference/campaigns/render
POST /api/v1/campaigns/{campaignId}/render
Render a campaign to email-ready HTML for an embedded preview
Render a campaign to the exact email-safe HTML that would be sent. Use this to show a visual preview inside your own dashboard or email builder without duplicating Sequenzy's rendering logic.
The render applies everything the send path applies: your email theme and font, company branding and footer, localization, block visibility conditions, product recommendation blocks, and merge tags.
This endpoint is read-only. It requires the `campaigns:read` scope and never
sends, schedules, or modifies anything. It uses `POST` only so personalization
input can travel in a request body.
## Request
Campaign ID.
Personalize as this stored subscriber. Use this or `subscriber`, not both.
Because the rendered HTML then carries that subscriber's details, this field
also requires the `subscribers:read` scope.
Personalize as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Tags this ad-hoc contact carries, used to evaluate `tag` block conditions.
Nothing is stored. Without it a tag condition has no tags to read, so only the
else branch of a tag split can be previewed.
Extra merge variables layered over the contact's attributes.
Force a localization locale instead of deriving it from the contact.
Render a specific A/B test variant of this campaign.
Apply your company's auto-UTM link decoration, as a real send would. Defaults
to `false` so the preview shows your own clean URLs.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/render" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subscriber": {
"email": "anna@example.com",
"firstName": "Anna"
},
"variables": { "plan": "Pro" },
"tracking": false
}'
```
## Personalization
Without `subscriberId` or `subscriber`, the campaign renders for a sample contact and merge tags resolve to empty values. The response's `personalized` field tells you which happened.
Only a stored subscriber (`subscriberId`) can resolve every piece of subscriber-scoped content: block conditions on stored state, product recommendations, and a working unsubscribe link. An inline `subscriber` covers name and attribute merge tags, and its `tags` cover `tag` block conditions.
Passing `subscriberId` puts that subscriber's data into the returned HTML, so it needs the `subscribers:read` scope on top of `campaigns:read`. Rendering for a sample contact or an inline `subscriber` needs only `campaigns:read`.
## Checking merge tags
An email never shows a raw `{{TAG}}` to a recipient, so a tag that does not exist and a tag that is simply blank for this contact both render as an empty string. `unresolvedMergeTags` tells them apart:
* `"reason": "unknown"` - nothing provides that name, so it will be empty for every recipient. Usually a typo, or a tag carried over from another platform.
* `"reason": "no_value"` - the name is recognized but has no value for this contact.
A tag with a fallback is reported too when its name is unrecognized. `{{ subscriber.frstName | default: "there" }}` renders `there` for everybody, including the recipients whose first name is stored, and the HTML looks correctly personalized - so an empty `unresolvedMergeTags` array, not the rendered greeting, is what tells you personalization works. A recognized name that is merely blank for this contact is not reported when it has a fallback, since that is exactly what a fallback is for.
A name is only called `unknown` when the render had a source to check it against. Without the contact's attributes nothing is checkable at all - a bare `{{plan}}` reads the same attribute map as `{{subscriber.plan}}` - so pass a stored `subscriberId`, or an inline `subscriber` that includes `customAttributes`. Beyond that, `{{event.*}}` needs sample event properties in `variables`, since a real send fills those from the enrolling event, and `{{recommendedProducts.*}}` needs a stored `subscriberId` the catalog has something to recommend for. Without those, the tags come back as `no_value` rather than being reported as typos. An optional attribute that this contact happens not to have set is kept out of `unknown` by checking the names other contacts in the account carry, which is subscriber data: that check needs the `subscribers:read` scope, so a key without it may report such a name as `unknown`.
An empty array means every tag in the email resolved.
## Checking conditions
Conditional blocks split on subscriber data, and the fields that read stored subscriber state - `tag`, `segment`, `list`, `status`, `event`, purchases, and engagement - are evaluated per recipient at send time. A render can evaluate them for a stored `subscriberId`, and it can evaluate a `tag` condition for an inline contact that lists its own `tags`. Anything else it cannot check.
A condition it cannot check renders as `false`, the same fail-closed rule a live send uses, so the else branch appears in the HTML exactly as it would for a contact who genuinely does not match. `unevaluatedConditions` is what tells those apart:
* `"reason": "requires_stored_subscriber"` - the field reads stored subscriber state. Pass `subscriberId`, or for a `tag` condition pass `subscriber.tags`.
* `"reason": "invalid_filter"` - the stored condition is malformed, so a real send fails it closed too. `hint` carries the validation error.
* `"reason": "evaluation_failed"` - the lookup itself failed. The render is worth retrying.
An empty array means every condition in the email was actually evaluated, so the branches in the HTML are the branches this contact would receive.
## What is not in the HTML
Per-send click-redirect wrapping, the open tracking pixel, and unsubscribe click tracking are applied at send time against a real email send record. A preview has no such record, so those are never present in the returned HTML - emitting placeholder versions would give you URLs that 404. The `tracking` flag therefore controls auto-UTM decoration only, which is genuinely applied at render time.
## Responses
```json 200 theme={null}
{
"success": true,
"html": "...",
"subject": "Anna, your Pro plan is ready",
"previewText": "Everything you unlocked this month",
"locale": "en",
"personalized": true,
"trackingApplied": false,
"unresolvedMergeTags": [{ "tag": "discountCode", "reason": "unknown" }],
"unevaluatedConditions": [
{
"field": "tag",
"operator": "contains",
"value": "extended",
"description": "Tag contains extended",
"reason": "requires_stored_subscriber",
"hint": "Pass subscriberId, or pass tags on the inline subscriber, to evaluate this condition."
}
],
"entity": {
"type": "campaign",
"id": "camp_abc123",
"variantId": null
}
}
```
```json 400 theme={null}
{
"error": "Provide either subscriberId or subscriber, not both."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
# Resend to Non-Openers
Source: https://docs.sequenzy.com/api-reference/campaigns/resend-to-non-openers
POST /api/v1/campaigns/{campaignId}/resend-to-non-openers
Create a draft resend to subscribers who didn't open a sent campaign
Every audience format keeps opener exclusions that manual additions cannot override. Resending a resend also preserves exclusions from earlier campaigns. Your audience is evaluated live, so new members can qualify even if they never received the original. If an older resend draft lacks `excludedCampaignOpenerIds`, recreate it from the original campaign and review the new draft before scheduling.
Create a draft that resends a sent campaign to everyone in the same audience who didn't open it. The draft copies the campaign email and reuses the original audience with a "didn't open this campaign" rule added, so only people who haven't opened it yet are targeted.
This is only available 6 hours after the campaign finishes sending, which gives opens time to register. It is never available for imported campaigns - ones created with a `sentAt` that predates the campaign record - because Sequenzy has no opens for a send it did not deliver. The response includes an estimate of how many subscribers haven't opened the original. The new draft is not sent automatically - review it, then schedule or send it.
## Request
ID of the sent campaign to resend to non-openers.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/resend-to-non-openers" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_new123",
"name": "April Launch (Non-openers)",
"subject": "A quick update",
"status": "draft",
"labels": [],
"emailId": "email_new123",
"scheduledAt": null,
"sentAt": null,
"createdAt": "2026-06-11T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_new123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_new123?step=review"
},
"estimatedNonOpenerCount": 842
}
```
```json 400 theme={null}
{
"error": "Resending to non-openers is only available 6 hours after a campaign finishes sending."
}
```
```json 400 theme={null}
{
"error": "This campaign was imported after it was sent elsewhere, so Sequenzy has no opens to resend against."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 500 theme={null}
{
"error": "Failed to create non-opener resend"
}
```
# Resume Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/resume
POST /api/v1/campaigns/{campaignId}/resume
Resume a paused campaign
Resume a campaign in `paused` status. Sending continues with the remaining recipients. When the campaign has a linked A/B test, the test or winner phase resumes from where it stopped.
Campaigns that were mid-send when company sending was paused (sender-health thresholds, moderation) or the account was banned are paused automatically. They cannot be resumed on their own while that company-level pause is still active: the request returns `409` and names the cause. Once company sending is restored, they restart automatically unless an admin chose to leave them paused, in which case this endpoint resumes them.
## Request
Campaign ID.
Spread the remaining delivery over this many hours. Must be an integer from 1
to 72. Pass `null` to clear an existing spread. Omit to keep the campaign's
current setting.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/resume" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"spreadOverHours": 6}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "sending",
"labels": [],
"scheduledAt": "2026-06-01T14:00:00.000Z",
"sentAt": null,
"createdAt": "2026-05-20T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"error": "Cannot resume a campaign in \"sending\" status."
}
```
```json 400 theme={null}
{
"error": "spreadOverHours must be an integer between 1 and 72."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 409 theme={null}
{
"error": "Campaign status has changed and can no longer be resumed."
}
```
```json 409 theme={null}
{
"error": "This campaign was paused because sending for this company is paused. Resume company sending first."
}
```
# Schedule Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/schedule
POST /api/v1/campaigns/{campaignId}/schedule
Schedule a campaign send
Schedule a draft or already scheduled campaign for a future send time. The campaign must have a sender profile whose sending domain is verified, an email with a subject and at least one content block, and an audience with at least one include rule.
The response returns `campaign.status`, which is either `scheduled` or
`waiting_approval`. Campaigns that require safety review are held in
`waiting_approval` and are scheduled once a reviewer approves them.
`waiting_approval` is a normal, successful (`200`) outcome, and it is most
common on **new accounts and recently registered sending domains**. Nothing is
sent while a campaign is held, and retrying the schedule call will not clear
the hold. Automated integrations should branch on `campaign.status` and then
poll [`GET /api/v1/campaigns/{campaignId}`](/api-reference/campaigns/get)
instead of retrying. See [Safety review](/concepts/campaigns#safety-review)
for what triggers the hold and how it clears.
## Request
Campaign ID.
Future ISO 8601 send time.
Optional targeting object, a union discriminated on `type`: `{"type":
"all"}`, `{"type": "lists", "listIds": ["list_123"]}`, `{"type": "segment",
"segmentId": "seg_123"}`, `{"type": "filtered", "filters": [...]}`, or
`{"type": "rules", "include": [...], "exclude": [...]}`. Omit this to reuse
saved campaign targeting - or, when none is saved, to send to ALL active
subscribers. Mutually exclusive with `listIds`.
Shorthand for sending to one or more lists, equivalent to `{"type": "lists",
"listIds": ["list_123"]}`. Mutually exclusive with `targetLists`.
Deliver each recipient at their predicted best open hour within
`sendTimeWindowHours` of `scheduledAt` (default 12h, max 24). Campaign-only:
there is no company or sequence STO toggle. Sequences use `sendingWindow`
instead. `spreadOverHours` takes precedence and turns STO off;
`sendInRecipientTimezone` also turns it off.
STO delivery window in hours from `scheduledAt` (1-24, default 12). Only used
when `sendTimeOptimization` is true. Recipients whose predicted hour falls
outside the window are snapped to the nearest edge.
Spread delivery over 1-72 hours. When set, spread delivery takes precedence
over send-time optimization.
Send to at most this many audience members (1-10,000,000). The first N
matching subscribers, ordered by subscriber id, receive the campaign after
every audience and suppression rule is applied; recipients already reached
count against the cap when a paused send resumes. Omit to keep the cap saved
on the draft, or send `null` to clear it. The returned
`campaign.estimatedRecipientCount` reflects the cap. Out-of-range values
return `400`. For A/B tests, the cap must allow at least one recipient per
variant plus one for the winner; a smaller saved or requested cap returns
`400` without changing the campaign or its schedule. To send the next batch,
duplicate the campaign and exclude contacts who already received it with an
"email sent" audience filter.
Deliver at `scheduledAt`'s wall-clock time in each recipient's own timezone -
"send when it's 8pm for the customer". Requires `scheduledTimezone`. Contacts
without a stored [subscriber timezone](/api-reference/subscribers/create)
receive the campaign at `scheduledAt` itself, and timezones whose local time
has already passed when processing starts receive it immediately. Not
combinable with `recurringInterval` or `spreadOverHours`; enabling it turns
off send-time optimization. Omitting it on a reschedule preserves the
campaign's existing setting; send `false` to turn it off.
IANA timezone the `scheduledAt` wall-clock time refers to, for example
`America/New_York`. Required with `sendInRecipientTimezone`.
Repeat the campaign `weekly` or `monthly` starting at `scheduledAt`. The
campaign becomes a recurring template - each run is duplicated and sent
automatically, re-evaluating audience membership every time. Omit for a
one-shot send; scheduling again without it stops the recurrence. Unschedule
the campaign to stop the series and return it to an editable draft.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/schedule" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-06-01T14:00:00Z",
"targetLists": {
"type": "all"
}
}'
```
Send to a single list via the `listIds` shorthand:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/schedule" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-06-01T14:00:00Z",
"listIds": ["list_abc123"]
}'
```
Repeat a campaign monthly to a segment:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/schedule" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-06-01T14:00:00Z",
"targetLists": {
"type": "segment",
"segmentId": "seg_abc123"
},
"recurringInterval": "monthly"
}'
```
Deliver at 8pm in each recipient's own timezone (8pm New York wall time):
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/schedule" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-06-02T00:00:00Z",
"sendInRecipientTimezone": true,
"scheduledTimezone": "America/New_York"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Campaign scheduled for 2026-06-01T14:00:00.000Z",
"scheduledAt": "2026-06-01T14:00:00.000Z",
"jobId": "mock-job-id",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review",
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "scheduled",
"labels": ["edm", "launch"],
"scheduledAt": "2026-06-01T14:00:00.000Z",
"recurringInterval": null,
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 200 theme={null}
{
"success": true,
"message": "Campaign is awaiting approval before it can be scheduled.",
"scheduledAt": "2026-06-01T14:00:00.000Z",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review",
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "waiting_approval",
"labels": ["edm", "launch"],
"scheduledAt": "2026-06-01T14:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Scheduled time must be in the future."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Sending domain is not verified. Please verify your domain before scheduling emails."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Campaign email subject is empty. Add a subject before sending."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Add at least one audience include rule before scheduling."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Campaign is no longer editable. It may have already started sending."
}
```
# Send Campaign Test
Source: https://docs.sequenzy.com/api-reference/campaigns/send-test
POST /api/v1/campaigns/{campaignId}/test
Send or preview a campaign test email
Queue a test send for a campaign. The API returns a durable `emailSendId`; use it with the email-send details endpoint to inspect `status` and `errorMessage`.
## Request
Campaign ID.
Email address that should receive the test.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "you@example.com"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Test email queued for you@example.com",
"campaignId": "camp_abc123",
"recipientEmail": "you@example.com",
"emailSendId": "send_abc123",
"jobId": "123"
}
```
`jobId` remains in the response for compatibility. Queue jobs are internal execution details and are not the delivery-status contract.
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
# Create Campaign Share Link
Source: https://docs.sequenzy.com/api-reference/campaigns/share-link-create
POST /api/v1/campaigns/{campaignId}/share-link
Create or fetch the campaign's public view-in-browser link
Create a public, shareable "view in browser" link for the campaign. The hosted page renders an anonymized copy - a sample contact instead of subscriber data, an inert unsubscribe link, and no open or click tracking - so you can forward the URL to anyone.
The call is idempotent: if a link is already active, the same URL is returned with `created: false` instead of rotating it, so copies you have already shared keep working. Revoke it with [Revoke Share Link](/api-reference/campaigns/share-link-revoke). The current link is also reported as `shareUrl` on [Get Campaign](/api-reference/campaigns/get) (`null` when none is active).
Email campaigns only - SMS campaigns have no browser view.
## Request
Campaign ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/share-link" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"shareUrl": "https://sequenzy.com/view/campaign/8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s",
"shareToken": "8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s",
"created": true
}
```
```json 400 theme={null}
{
"error": "Only email campaigns have a browser view to share."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
# Revoke Campaign Share Link
Source: https://docs.sequenzy.com/api-reference/campaigns/share-link-revoke
DELETE /api/v1/campaigns/{campaignId}/share-link
Revoke the campaign's public view-in-browser link
Revoke the campaign's public share link. The hosted URL returns 404 immediately. Creating a new link later mints a different URL - previously distributed copies stay dead.
Returns `revoked: false` when the campaign had no active link, which is not an error.
## Request
Campaign ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/share-link" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"revoked": true
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
# Unschedule Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/unschedule
POST /api/v1/campaigns/{campaignId}/unschedule
Return a scheduled campaign to an editable draft
Unschedule a campaign in `scheduled` status. The pending send is removed, any
weekly or monthly recurrence is stopped, and the campaign returns to `draft` so
you can edit and schedule it again. This is different from
[cancelling a campaign](/api-reference/campaigns/cancel), which is permanent.
## Request
Campaign ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/campaigns/camp_abc123/unschedule" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"status": "draft",
"labels": [],
"scheduledAt": null,
"sentAt": null,
"createdAt": "2026-05-20T10:00:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"error": "Cannot unschedule a campaign in \"draft\" status."
}
```
```json 404 theme={null}
{
"error": "Campaign not found"
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
```json 409 theme={null}
{
"success": false,
"code": "CAMPAIGN_UNSCHEDULE_CONFLICT",
"error": "Campaign can no longer be unscheduled",
"title": "Campaign can no longer be unscheduled",
"description": "Delivery has already started or the campaign scheduling state changed while the request was processed.",
"resolution": "Call get_campaign to refresh status. If it is still scheduled, retry unschedule_campaign once. If it is sending, use pause_campaign or cancel_campaign. Do not retry in a loop."
}
```
# Update Campaign
Source: https://docs.sequenzy.com/api-reference/campaigns/update
PUT /api/v1/campaigns/{campaignId}
Update a draft campaign's content, audience, labels, sending identity, or delivery pacing
If you use an account key as a marketer, choose an existing sender and
Reply-To profile. A request that needs a new profile returns `400` before
creating profiles, labels, or campaign/sequence changes. This also applies to
email steps and inserted sequence branches. An owner or admin can add the
identity first; a step-level display-name override can still use an existing
profile.
Update a draft campaign. You can change the campaign name, subject, labels, HTML body, Sequenzy block content, Style > Format, campaign data, computed lists, audience, From profile, Reply-To profile, or Send Time Optimization. Direct addresses create a backing profile when needed. STO is campaign-only; sequences use `sendingWindow`.
## Request
Campaign ID.
Updated campaign name.
Updated email subject line.
Updated inbox preview text. Send `null` to clear it.
Alias for `previewText`.
Campaign tracking code available to UTM templates as
`{{campaign.trackingCode}}`. Send an empty string or `null` to clear it.
Updated HTML body. Sequenzy converts this into email blocks.
Updated Sequenzy email blocks. Use this instead of `html` when you need
editor-compatible content or conditional blocks. Do not send both `html` and
`blocks` in the same request. Put block styling under `styles`; top-level
style keys like `backgroundColor`, `backgroundOpacity`, `textColor`,
`textAlign`, `borderColor`, `borderWidth`, and `borderRadius` are normalized
into `styles`.
Change Style > Format to `branded` or `minimal` without rewriting the
campaign copy. Supported for native Sequenzy blocks, including supported
custom HTML blocks, but not a standalone raw HTML email; do not combine it
with `html`. Applying `minimal` removes standalone logo blocks. Switching back
to `branded` generates a new logo unless you send the authored logo block
again in `blocks`.
Replacement campaign audience, using the same shapes as
[create](/api-reference/campaigns/create). Send `null` to clear saved
targeting and pick the audience when you schedule. Omit it to leave the saved
audience unchanged. Mutually exclusive with `segmentId` and `listIds`.
Shorthand for retargeting the draft at one saved segment, equivalent to
`{"type": "segment", "segmentId": "seg_123"}`. Mutually exclusive with
`targetLists` and `listIds`.
Shorthand for retargeting the draft at one or more lists, equivalent to
`{"type": "lists", "listIds": ["list_123"]}`. Mutually exclusive with
`targetLists` and `segmentId`.
Deliver each recipient at their predicted best open hour within
`sendTimeWindowHours` of `scheduledAt`. Campaign-only: there is no company or
sequence STO toggle. Sequences use `sendingWindow` instead. Persists on the
draft until `schedule` overrides it. `spreadOverHours` and
`sendInRecipientTimezone` each turn STO off.
STO delivery window in hours from `scheduledAt` (1-24, default 12). Only used
when `sendTimeOptimization` is true.
Send to at most this many audience members (1-10,000,000). The first N
matching subscribers, ordered by subscriber id, receive the campaign after
every audience and suppression rule is applied. Persists on the draft until
[Schedule Campaign](/api-reference/campaigns/schedule) overrides it. Send
`null` to remove the limit. Out-of-range values return `400`.
Replacement label names. Send an empty array to clear labels. Missing labels
are created automatically. The API also accepts `label` as a compatibility
alias.
Reply-To email address for this campaign. Use this or `replyProfileId`, not
both.
Display name for the Reply-To address. Requires `replyTo`; omit it when you
send `replyProfileId`, which already carries its own display name. An address
carries one Reply-To name company-wide: if `replyTo` already has a saved
profile under a different name, that saved name is kept and the response
`warnings` array says so.
Reply profile ID for this company. It already supplies both the Reply-To
address and the display name, so send it on its own and omit `replyTo` and
`replyToName`.
From address for this campaign. Its domain must be configured and verified.
Use this or `senderProfileId`, not both.
Display name recipients see, e.g. `Brennon at TradeTally`. Selects the sender
identity of that name on `fromEmail`, creating it when the address has no
identity by that name; the mailbox's other display names, and everything
pinned to them, are untouched. Requires `fromEmail`; omit it when you send
`senderProfileId`, which already carries its own display name.
Existing sender profile ID. It already supplies both the From address and the
display name, so send it on its own and omit `fromEmail` and `fromName`.
Up to 10 addresses CC'd on every recipient's email for this campaign. Send an
empty array or null to clear them. The recipient themselves is never copied.
Up to 10 addresses BCC'd on every recipient's email for this campaign. Send an
empty array or null to clear them.
Campaign-scoped JSON data. For example, upload an `events` array that this
campaign can use while rendering repeat blocks. Supports up to 500 items per
top-level array. Send `null` to clear it; omit it to keep the current data.
Personalized list definitions that select items from `campaignData` using
subscriber attributes. Templates can repeat over each list's `key`; keys use
letters, numbers, underscores, and dots only. Use `maxItems` to cap how many
items each subscriber can receive in that list.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"id": "pro-offer",
"type": "text",
"content": "
Your Pro upgrade is ready.
",
"variant": "paragraph",
"conditions": [
{ "id": "c1", "field": "variable", "operator": "is", "value": "plan:pro" }
]
}
]
}'
```
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"preheaderText": "A short preview for the inbox"}'
```
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emailPreset": "minimal"}'
```
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"labels": ["edm", "launch"]}'
```
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"targetLists": {"type": "lists", "listIds": ["list_abc123"]}}'
```
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/campaigns/camp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaignData": {
"events": [
{
"id": "evt_123",
"title": "Romeo and Juliet",
"region": "Auckland",
"genre": "theatre",
"url": "https://example.com/events/evt_123"
}
]
},
"computedLists": [
{
"key": "recommendedEvents",
"source": "events",
"maxItems": 6,
"rules": [
{
"itemField": "region",
"operator": "equals",
"subscriberField": "region"
},
{
"itemField": "genre",
"operator": "in",
"subscriberField": "interests"
}
]
}
]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "camp_abc123",
"name": "April Launch",
"subject": "A quick update",
"emailId": "email_abc123",
"emailPreset": "minimal",
"status": "draft",
"labels": ["edm", "launch"],
"targetLists": { "type": "lists", "listIds": ["list_abc123"] },
"senderProfileId": "sender_abc123",
"fromName": "Acme",
"fromEmail": "hello@example.com",
"replyProfileId": "reply_abc123",
"replyToName": "Support",
"replyToEmail": "support@example.com",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123",
"previewUrl": "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide either replyProfileId or replyTo, not both. replyProfileId already sets both the Reply-To address and the display name, so send it without replyTo and replyToName. To use a different address or display name, omit replyProfileId and send replyTo (optionally with replyToName) instead."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at least one of name, subject, previewText, preheaderText, html, blocks, emailPreset, fromEmail, senderProfileId, replyTo, replyProfileId, ccEmails, bccEmails, trackingCode, campaignData, computedLists, targetLists, segmentId, listIds, labels, sendTimeOptimization, or sendTimeWindowHours."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at most one of segmentId, listIds, or targetLists. Received: segmentId, targetLists."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide either previewText or preheaderText, not both."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Rule must provide subscriberField or value unless operator is exists"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Campaign content: provide either html or blocks, not both."
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/replyTo",
"message": "Expected string to match 'email' format",
"summary": "Property 'replyTo' should be email"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Campaign not found"
}
```
Explicit `targetLists.includedSubscriberIds` override exclusions. To remove a contact from a frozen group, remove its ID from that array.
# Create Company
Source: https://docs.sequenzy.com/api-reference/companies/create
POST /api/v1/companies
Create a company workspace
Create a company workspace. Provide a website domain to queue brand processing,
or set `noWebsite` and provide a name to start without one. Set
`withWelcomeSequence` to create and generate the four-email welcome sequence;
the optional `description` grounds that sequence when no website is available.
This endpoint requires a personal account key (`seq_user_...`). A
company-scoped key (`seq_live_...` or a legacy `ek_...` key) is bound to the
single company it was created for, so it is rejected with `403` - it could
never access the workspace it created.
Creating another company does not create another core email subscription.
Companies owned by the same account share its plan and monthly email usage.
See [Workspaces, Billing, and Shared
Limits](/concepts/workspaces-and-billing).
## Request
Company website domain or URL. Required unless `noWebsite` is `true`. When you
send both, Sequenzy uses the domain.
Company display name. If omitted, Sequenzy derives it from the domain. Website
processing may later replace it with the name found on your site. Required
when `noWebsite` is `true` and you send no domain.
Set to `true` (any other value is ignored) to create without a website. The
workspace is ready at once (`status: "processed"`) and no website processing
is queued. `name` is required when you send no domain.
Optional business description, up to 500 characters, stored trimmed as product
context for the welcome-sequence generator. Without a website, a non-null
value that is not a string or is longer than 500 characters returns `400`.
With a domain, such a value is ignored, and website processing may later
replace the description with the one found on your site.
Set to `true` to create a draft four-email welcome sequence and queue its
generation. Defaults to `false`; any other value is treated as `false`. The
sequence stays a draft until you turn it on. If the sequence cannot be
created, the company is still created and both welcome fields are `null`. If
only queueing fails, `welcomeAutomationId` is set and `welcomeEnrichmentJobId`
is `null`; open the sequence in the editor to retry generation.
```bash With a website theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "acme.com", "name": "Acme", "withWelcomeSequence": true}'
```
```bash Without a website theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"noWebsite": true, "name": "Night Shift Meals", "description": "Weekly meal plans for night-shift nurses", "withWelcomeSequence": true}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"company": {
"id": "company_abc123",
"name": "Acme",
"status": "processing",
"websiteUrl": "https://acme.com"
},
"welcomeAutomationId": "automation_abc123",
"welcomeEnrichmentJobId": "job_abc123",
"message": "Company created. Processing your website to extract brand information. This typically takes 30-60 seconds."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 400 theme={null}
{
"error": "Name is required when starting without a website."
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/name",
"message": "Expected string",
"summary": "Expected property 'name' to be string but found: 42"
}
```
```json 403 theme={null}
{
"error": "This API key is scoped to a single company, so it cannot create another one. The new company would be created under the key owner's account, but this key could never list, select, or use it. Retry with a personal account key (seq_user_...) from Account -> API Keys, or create the company in the dashboard.",
"code": "COMPANY_CREATE_REQUIRES_PERSONAL_KEY"
}
```
```json 500 theme={null}
{
"error": "Failed to create company"
}
```
# Get Company
Source: https://docs.sequenzy.com/api-reference/companies/get
GET /api/v1/companies/{companyId}
Get one company workspace
Get company details for a company the authenticated key can access.
`emailBranding` reports the effective, read-only "Sent with Sequenzy"
entitlement. `visible` is what future renders and sends will show;
`removalEntitled`, `reason`, `subscriptionTier`, and `subscriptionStatus`
explain why. Use `subscriptionUrl` for the owner-facing upgrade or billing
handoff. The badge is injected when an email is rendered, so an entitlement
change also applies to future sends from existing live sequences without
editing their stored footer blocks.
The response includes `defaultSubscriberListIds`, the workspace default lists new contacts join when nothing targets them explicitly: `null` means every current and future list, `[]` means no list at all, and an array means exactly those lists. Read it before connecting an integration whose provider has no per-integration list targeting of its own: contacts created by live ingestion and payment-provider backfills land wherever this points. PostHog history imports are an exception and create contacts without list memberships. Change the setting with [update company](/api-reference/companies/update).
## Request
Company ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/companies/company_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"company": {
"id": "company_abc123",
"name": "Acme",
"status": "ready",
"websiteUrl": "acme.com",
"logoUrl": "https://example.com/logo.png",
"primaryColor": "#0ea5e9",
"brandColors": { "primary": "#0ea5e9" },
"description": "Lifecycle email automation for SaaS teams.",
"toneVoice": "clear, direct, warm",
"companyContext": "Acme helps SaaS teams send lifecycle emails from product events.",
"emailDesignPrompt": "Editorial and calm. One idea per email, no feature grids.",
"emailLengthPreference": "concise",
"defaultSenderProfileId": "sender_abc123",
"defaultFromName": "Acme",
"defaultFromEmail": "hello@acme.com",
"defaultReplyProfileId": "reply_abc123",
"defaultReplyToName": "Acme Support",
"defaultReplyToEmail": "support@acme.com",
"replyTrackingEnabled": true,
"replyTrackingDomainMode": "sequenzy",
"forwardReplies": true,
"replyRetentionDays": 30,
"defaultSubscriberListIds": ["list_abc123"],
"emailBranding": {
"visible": true,
"removalEntitled": false,
"reason": "free_plan",
"managedBy": "owner_subscription",
"removalAction": "upgrade",
"subscriptionTier": null,
"subscriptionStatus": null,
"subscriptionUrl": "https://sequenzy.com/dashboard/company/company_abc123/account?tab=subscription"
},
"createdAt": "2026-05-01T10:30:00Z",
"language": "en",
"emailLocalizationConfig": {
"enabled": false,
"locales": []
}
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Company not found"
}
```
# List Companies
Source: https://docs.sequenzy.com/api-reference/companies/list
GET /api/v1/companies
List companies available to the API key
List the companies the authenticated key can access.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"companies": [
{
"id": "company_abc123",
"name": "Acme",
"status": "ready",
"websiteUrl": "acme.com",
"logoUrl": "https://example.com/logo.png",
"primaryColor": "#0ea5e9",
"brandColors": { "primary": "#0ea5e9" },
"description": "Lifecycle email automation for SaaS teams.",
"toneVoice": "clear, direct, warm",
"companyContext": "Acme helps SaaS teams send lifecycle emails from product events.",
"emailDesignPrompt": "Editorial and calm. One idea per email, no feature grids.",
"emailLengthPreference": "concise",
"replyTrackingEnabled": true,
"replyTrackingDomainMode": "sequenzy",
"forwardReplies": true,
"replyRetentionDays": 30,
"createdAt": "2026-05-01T10:30:00Z",
"language": "en",
"emailLocalizationConfig": {
"enabled": false,
"locales": []
}
}
],
"currentCompanyId": "company_abc123"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Update Company
Source: https://docs.sequenzy.com/api-reference/companies/update
PATCH /api/v1/companies/{companyId}
Update product info, brand context, reply tracking, default lists, and default sending identity
Update company product info, brand context, reply-tracking settings, the workspace default lists, and account-wide From and Reply-To defaults. A new profile is created when an address does not already have one. From addresses must use a configured, verified sending domain; Reply-To addresses may use any valid mailbox. The current read-only retention period is returned as `replyRetentionDays` by the company endpoints.
Requires the `company_profile:manage` scope. The sending-identity and reply-tracking fields (`senderProfileId`, `fromEmail`, `fromName`, `replyProfileId`, `replyTo`, `replyToName`, `replyTrackingEnabled`, `replyTrackingDomainMode`, `forwardReplies`) and `defaultSubscriberListIds` additionally require `companies:manage`; a request that mixes them with profile fields is rejected as a whole rather than partially applied. Keys that already hold `companies:manage` satisfy `company_profile:manage` automatically.
The response includes the same read-only `emailBranding` entitlement as
[get company](/api-reference/companies/get). It is derived from the owner's
subscription and cannot be changed by adding a branding field to this request.
Omit a field to preserve its value. For nullable profile fields such as
`description`, `companyContext`, `toneVoice` and `valueProps`, send `null` to
clear the value. Arrays and profile objects replace their previous values;
`emailTheme` is a partial update as described below.
## Request
Company ID.
Workspace name. Must not be blank.
Set to true with `name` and `previousCompanyName` to rename exact matching
company names in editable email footer blocks and saved components. Includes
nested footers, draft A/B variants and their localizations. Custom names, body
copy, sent or sending campaigns and active A/B tests stay unchanged. Null is
not accepted. Omit or pass false to change only company settings.
Required when `renameMatchingFooters` is true. Use the current name from get
company. A stale name returns 409 without applying changes. Refresh and
confirm the replacement again. Retrying after a successful rename does not
repeat footer updates. The name and footer updates commit together; no emails
are sent.
Company logo URL. Send `null` to clear it.
Founder name for your company profile and generated copy. Send `null` to clear
it.
Replacement brand-color data, including optional `footerTheme`. Read and merge
the current object before updating one entry; this field is replaced
wholesale. Send `null` to clear it.
Replacement testimonial data for your company profile and AI context. Send
`null` to clear it.
Preferred generated email length: `concise`, `balanced`, or `detailed`.
Replacement map of social network names to URL strings. Send `null` to clear
it.
Privacy policy URL. Send `null` to clear it.
Terms URL. Send `null` to clear it.
Company postal address used in marketing footers. Send `null` to clear it.
Default language for generated email copy, such as `en`. Must not be blank.
Replacement product pricing context for generated copy. Send `null` to clear
it.
Default email font family. Send `null` to clear the override.
Default email text direction: `ltr` or `rtl`.
Primary brand color as a 6-digit hex value, for example `#0ea5e9`. Also
updates the email theme's primary color unless `emailTheme` sets one
explicitly.
Default email theme applied to campaigns, sequences, and transactional
email. Partial update: omitted fields keep their current value (or the
preset default), and numeric values are clamped to supported ranges. Pass
`null` to reset to the platform default theme. Supported fields:
* `presetId`: `default`, `soft`, `editorial`, or `bold`
* `buttonStyle`: `solid` (filled, the default) or `outline` (transparent
fill with a brand-color border)
* `colors`: 6-digit hex values for `primary`, `background` (outer canvas),
`content` (inner content card; omit to preserve its current value, or to
follow the canvas when no content color is stored), `surface` (nested cards
and tinted tiles), `text`, `mutedText`, `heading`, `border`, `link`, and
optionally `buttonText` (solid button label color; omit to auto-derive a
readable color from the button fill)
* `typography`: numbers for `baseFontSize`, `leadFontSize`,
`baseLineHeight`, `heading1Size`, `heading2Size`, `heading3Size`,
`buttonFontSize`, `buttonFontWeight` (400-800), `headingFontWeight`
(300-900, applied to all heading levels; omit for the per-level defaults),
and `headingLetterSpacing` (px, -2 to 4, negative = tighter tracking);
plus `headingFontFamily` as a font-stack string for serif-headline +
sans-body pairings (omit so headings inherit the email font)
* `layout`: numbers for `contentWidth`, `containerPaddingX`,
`containerPaddingY`, `blockSpacing`, `baseRadius`, `buttonRadius`,
`sectionPadding`, `buttonPaddingX`, `buttonPaddingY`,
`borderedBlockPadding`
The auto-appended unsubscribe footer has its own colors stored under
`brandColors.footerTheme` (`backgroundColor`, `companyTextColor`,
`metaTextColor`, `linkColor`, `borderColor`). When changing them via
`brandColors`, send the merged object - the field is replaced wholesale.
Short product or company summary.
Basic product/company knowledge AI should use when writing emails.
Tone of voice guidance for generated copy.
Art direction for AI-designed emails: layout, density, which sections belong
in an email, imagery, and CTA prominence. `toneVoice` steers the copy; this
steers the design. When empty, the next email generation prefills it with the
direction derived from the brand. Send `null` to clear it - the next
generation writes a fresh one.
Value propositions, usually objects with `title` and `description`.
Existing sender profile to make the account-wide default, and the profile
`fromName` renames. List IDs with [GET
/v1/sender-profiles](/api-reference/sender-profiles/list). Mutually exclusive
with `fromEmail`.
Account-wide default From address. Its domain must already be configured and
verified.
Display name of the default From profile. Sent on its own it renames the
current default profile without changing its address; with `senderProfileId`
it renames that profile; with `fromEmail` it names the profile for that
address. If the address already carries several display names, the request is
rejected - pass `senderProfileId` to say which one to rename.
Existing reply profile to make the account-wide default, and the profile
`replyToName` renames. Mutually exclusive with `replyTo`.
Account-wide default Reply-To address.
Display name of the default Reply-To profile. Sent on its own it renames the
current default profile without changing its address; with `replyProfileId` it
renames that profile; with `replyTo` it names the profile for that address.
Turn the inbox on or off for this company. When on, replies to your emails and
email sent to the [inbox address](../conversations/inbox-address) are stored
as conversations.
Reply domain mode: `sequenzy` for the managed inbound domain or `custom` for a
configured custom domain.
Enable or disable forwarding captured replies to the configured mailbox.
Which lists new contacts join when something creates a subscriber without
explicit list targeting - forms, API writes, events, tag actions, imports,
live ingestion from integrations without their own list targeting, and
payment-provider backfills. PostHog history imports are an exception and
create contacts without list memberships.
The three states are different and none is a synonym for another:
* `null`: every current and future list, including private lists
* `[]`: no list at all
* populated array: exactly those lists
Unknown IDs, and IDs belonging to another company, are rejected rather than
skipped - the whole request fails so you never end up defaulting to fewer
lists than you asked for. Accepts at most 100 lists.
Changing this affects only later writes. Nobody is moved or removed from a
list retroactively, so a list already filled by an integration backfill still
needs [remove subscribers from a
list](/api-reference/lists/remove-subscribers).
In the dashboard the same setting lives under **Contacts -> Lists -> Default
Lists**.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/companies/company_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"primaryColor": "#0ea5e9",
"emailTheme": {
"colors": { "mutedText": "#4b5563" },
"typography": { "baseFontSize": 16 }
},
"companyContext": "Acme helps SaaS teams send lifecycle emails from product events.",
"toneVoice": "clear, direct, warm",
"emailDesignPrompt": "Editorial and calm. One idea per email, no feature grids.",
"fromEmail": "hello@acme.com",
"fromName": "Acme",
"replyTo": "support@acme.com",
"replyToName": "Acme Support",
"replyTrackingEnabled": true,
"replyTrackingDomainMode": "sequenzy",
"forwardReplies": true,
"defaultSubscriberListIds": ["list_abc123"]
}'
```
Keep new contacts out of every list unless something targets them explicitly:
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/companies/company_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "defaultSubscriberListIds": [] }'
```
## Responses
```json 200 theme={null}
{
"success": true,
"company": {
"id": "company_abc123",
"name": "Acme",
"primaryColor": "#0ea5e9",
"brandColors": { "primary": "#0ea5e9" },
"companyContext": "Acme helps SaaS teams send lifecycle emails from product events.",
"toneVoice": "clear, direct, warm",
"emailDesignPrompt": "Editorial and calm. One idea per email, no feature grids.",
"emailLengthPreference": "concise",
"defaultSenderProfileId": "sender_abc123",
"defaultFromName": "Acme",
"defaultFromEmail": "hello@acme.com",
"defaultReplyProfileId": "reply_abc123",
"defaultReplyToName": "Acme Support",
"defaultReplyToEmail": "support@acme.com",
"replyTrackingEnabled": true,
"replyTrackingDomainMode": "sequenzy",
"forwardReplies": true,
"replyRetentionDays": 30,
"defaultSubscriberListIds": ["list_abc123"],
"emailBranding": {
"visible": false,
"removalEntitled": true,
"reason": "paid_plan",
"managedBy": "owner_subscription",
"removalAction": "none",
"subscriptionTier": "pro_1k",
"subscriptionStatus": "active",
"subscriptionUrl": "https://sequenzy.com/dashboard/company/company_abc123/account?tab=subscription"
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "primaryColor must be a 6-digit hex color such as #f97316."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unknown list IDs in defaultSubscriberListIds: list_missing."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: companies:manage. It is required to change account-wide sending defaults: fromEmail. Update the remaining workspace profile fields on their own, or retry with a broader key."
}
```
```json 404 theme={null}
{
"success": false,
"error": "Company not found"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Company name changed. Refresh company details before renaming matching footers."
}
```
Only the new `renameMatchingFooters: true` option additionally requires `emails:write`, `templates:write`, `campaigns:write`, `sequences:write`, `transactional:write` and `ab_tests:write`. Personal keys also require workspace owner or admin access. Ordinary company updates keep their existing permissions.
# Update Conversation Statuses
Source: https://docs.sequenzy.com/api-reference/conversations/bulk-update-status
POST /api/v1/conversations/bulk/status
Open or close up to 100 conversations in one request
Open or close several conversations at once, for example every conversation on one page of [List Conversations](./list). Only conversations whose status changes are updated, so you can safely retry a request after a timeout: conversations that already have the status are reported as unchanged.
Closed conversations are reopened automatically when the subscriber replies or when you send a new outbound message.
## Request
Conversation IDs to update, up to 100 distinct IDs. Surrounding whitespace is
trimmed, and blank or repeated IDs are ignored before the limit is checked. At
least one non-blank ID is required.
New status for every listed conversation: `open` or `closed`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/bulk/status" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"conversationIds": ["conv_abc123", "conv_def456", "conv_missing"], "status": "closed"}'
```
## Response
`true` when the request was processed.
The status that was applied: `open` or `closed`.
Number of distinct conversation IDs in the request.
Number of conversations whose status changed.
Number of conversations that already had the status.
Number of IDs that do not exist in your company. They are skipped; the rest of
the request still applies.
IDs whose status changed, in request order.
IDs that already had the status, in request order.
IDs that do not exist in your company, in request order.
```json 200 theme={null}
{
"success": true,
"status": "closed",
"requested": 3,
"updated": 1,
"unchanged": 1,
"notFound": 1,
"updatedIds": ["conv_abc123"],
"unchangedIds": ["conv_def456"],
"notFoundIds": ["conv_missing"]
}
```
```json 400 theme={null}
{
"error": "Provide at least one conversation ID"
}
```
```json 400 (too many IDs) theme={null}
{
"error": "Provide at most 100 conversation IDs"
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/status",
"message": "Expected union value",
"summary": "Property 'status' must be one of: open, closed"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: conversations:write"
}
```
Requests whose `conversationIds` array has no usable IDs or more than 100 distinct IDs return `400`. A missing or unknown `status`, or a missing or non-array `conversationIds`, returns `422`. Rejected requests change no conversations. The API key needs the `conversations:write` scope.
# Forward Conversation Message
Source: https://docs.sequenzy.com/api-reference/conversations/forward
POST /api/v1/conversations/{conversationId}/messages/{messageId}/forward
Email a copy of one message to another address
Email a copy of one received (`inbound`) or sent (`outbound`) message to another address, such as a teammate. Stored attachments are included up to 15 MB in total; files that are not stored, cannot be read, or do not fit are left out and listed in the forwarded email. Notes and system messages cannot be forwarded.
The forward is recorded in the conversation as a `system` message ("Forwarded to …") whose `deliveryStatus` moves from `pending` to `sent`, or to `failed` if it cannot be sent. It does not change the conversation's status, unread state or last activity, and the recipient's replies are not tracked in the conversation. Each request sends a new forward, so retrying a successful request sends another copy. A forward counts toward your sending usage and triggers the `email.sent` webhook, like other sent email. Replies from the recipient are not added to the conversation.
Requires the `conversations:write` scope.
## Request
Conversation ID.
ID of the message to forward, from [Get Conversation](./get).
One email address to forward the message to.
Sender profile to forward from. Its sending domain must be verified. When
omitted, the company default sender is used. List profiles with [List Sender
Profiles](../sender-profiles/list).
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/messages/msg_abc123/forward" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "sales@example.com"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"forward": {
"id": "msg_fwd123",
"conversationId": "conv_abc123",
"messageId": "msg_abc123",
"to": "sales@example.com",
"deliveryStatus": "pending",
"createdAt": "2026-09-30T10:00:00.000Z"
}
}
```
```json 400 (invalid recipient) theme={null}
{
"error": "Provide one valid email address to forward to.",
"code": "invalid_recipient"
}
```
```json 400 (note) theme={null}
{
"error": "Only received and sent emails can be forwarded; notes stay internal.",
"code": "message_not_forwardable"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Message not found in this conversation",
"code": "message_not_found"
}
```
# Get Conversation
Source: https://docs.sequenzy.com/api-reference/conversations/get
GET /api/v1/conversations/{conversationId}
Get a conversation with all messages
Get one conversation with its full message history, the originating campaign or sequence context, and the subscriber's details.
Message `type` is `inbound` for received email, `outbound` for team replies, `note` for internal notes, and `system` for recorded forwards. `context.type` is `inbox` when the conversation started with an email sent to your [inbox address](./inbox-address).
Each attachment has `filename`, `contentType` and `size`. Stored files also have a `downloadUrl`, a signed link valid for one hour. `downloadUrl` is `null` when the file was not stored: attachment storage is not configured, the file failed the virus scan, it was larger than 25 MB, it was beyond the first 20 files of the message, storing it failed, or its retention period ended.
## Request
Conversation ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/conversations/conv_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"conversation": {
"id": "conv_abc123",
"companyId": "comp_abc123",
"subscriberId": "sub_abc123",
"subscriberEmail": "user@example.com",
"subscriberName": "John Doe",
"subject": "Re: A quick update",
"status": "open",
"messageCount": 2,
"lastMessageAt": "2026-06-11T09:30:00.000Z",
"lastMessageBy": "subscriber",
"hasUnread": true,
"initialEmailSendId": "send_abc123",
"initialCampaignId": "camp_abc123",
"initialAutomationNodeId": null,
"createdAt": "2026-06-10T15:00:00.000Z",
"updatedAt": "2026-06-11T09:30:00.000Z",
"expiresAt": null,
"messages": [
{
"id": "msg_abc123",
"type": "inbound",
"subject": "Re: A quick update",
"bodyText": "Thanks, this is great!",
"bodyHtml": null,
"fromEmail": "user@example.com",
"fromName": "John Doe",
"fromUserId": null,
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"size": 48213,
"s3Key": "attachments/comp_abc123/5f0c.../invoice.pdf",
"downloadUrl": "https://sequenzy-inbox.s3.amazonaws.com/attachments/...&X-Amz-Signature=..."
}
],
"isRead": false,
"readAt": null,
"readBy": null,
"deliveryStatus": null,
"deliveredAt": null,
"createdAt": "2026-06-11T09:30:00.000Z"
}
],
"context": {
"type": "campaign",
"campaignId": "camp_abc123",
"campaignName": "April Launch",
"automationNodeId": null,
"automationName": null,
"initialEmailSubject": "A quick update"
},
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"tags": ["premium"],
"customAttributes": { "plan": "pro" }
}
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Conversation not found"
}
```
# Get Inbox Address
Source: https://docs.sequenzy.com/api-reference/conversations/inbox-address
GET /api/v1/conversations/inbox-address
Get the address anyone can email to reach your inbox
Get the email address that delivers straight to your inbox. People, services and other AI agents can write to it; each message opens a new conversation, or joins the conversation it answers. Replies to your campaigns, sequences and transactional email arrive in the same inbox through their own reply-to addresses.
The default address is `inbox+{companyId}@inbound.sequenzy.com` and always works. You can also choose a branded address such as `support@inbound.acme.com` on one of your verified sending domains with [Update Inbox Address](./update-inbox-address). Once it is active, `address` returns it. `addresses` lists every address that currently delivers to this inbox.
Email only arrives while receiving is on (`enabled`). Turn it on in **Settings → Inbox**, or with [Update Company](../companies/update) and `replyTrackingEnabled`. Email sent to the inbox address does not trigger `email.replied` webhooks or automations and is not auto-forwarded. Mail that fails the spam or virus scan is dropped.
Requires the `conversations:read` scope.
## Request
This endpoint takes no parameters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/conversations/inbox-address" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Whether the company receives email. When `false`, replies and inbox address
email are not stored.
The address to share: your branded address once it is active, otherwise the
default address.
`inbox+{companyId}@inbound.sequenzy.com`. Always works while receiving is on.
Every address that currently delivers to this inbox, starting with `address`.
The branded address you chose, or `null`: `localPart`, `domain`, `address`
(`null` until the domain's inbound host is known) and `status` (`active` once
its inbound MX record is verified, otherwise `pending`).
Whether attachment content is stored, so attachments can be downloaded,
forwarded and sent with replies.
Days received email and its attachments are kept. At least 90.
## Responses
```json 200 theme={null}
{
"success": true,
"inbox": {
"enabled": true,
"address": "support@inbound.example.com",
"defaultAddress": "inbox+comp_abc123@inbound.sequenzy.com",
"addresses": [
"support@inbound.example.com",
"inbox+comp_abc123@inbound.sequenzy.com",
"inbox+comp_abc123@inbound.example.com"
],
"customAddress": {
"localPart": "support",
"domain": "example.com",
"address": "support@inbound.example.com",
"status": "active"
},
"attachmentsAvailable": true,
"retentionDays": 90
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: conversations:read"
}
```
# List Conversations
Source: https://docs.sequenzy.com/api-reference/conversations/list
GET /api/v1/conversations
List inbox conversations
List inbox conversations: replies to your campaigns, sequences and transactional email, plus email sent to your [inbox address](./inbox-address). Results are sorted by most recent message and can be filtered by status, unread flag, or a search term.
`context.type` is `campaign`, `sequence` or `transactional` for replies, `inbox` (label `Inbox`) for email sent to the inbox address, and `unknown` otherwise.
## Request
Filter by status: `all`, `open`, or `closed`. Defaults to `all`.
Search in subject, subscriber email, or subscriber name.
Page number. Defaults to 1.
Results per page, from 1 to 100. Defaults to 20.
Pass `true` to only return conversations with unread messages.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/conversations?status=open&unread=true" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
A plain-text preview of the latest subscriber or team reply, up to 200 characters. Quoted history, signatures and markup are removed, and internal notes are skipped. `null` when no reply has readable text.
`inbound` for a subscriber reply, `outbound` for a team reply.
The preview text.
## Responses
```json 200 theme={null}
{
"success": true,
"conversations": [
{
"id": "conv_abc123",
"subscriberId": "sub_abc123",
"subscriberEmail": "user@example.com",
"subscriberName": "John Doe",
"subject": "Re: A quick update",
"status": "open",
"messageCount": 3,
"lastMessageAt": "2026-06-11T09:30:00.000Z",
"lastMessageBy": "subscriber",
"hasUnread": true,
"createdAt": "2026-06-10T15:00:00.000Z",
"lastMessagePreview": {
"type": "inbound",
"text": "Thanks, Thursday works for me."
},
"context": {
"type": "campaign",
"label": "April Launch"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
```
```json 400 theme={null}
{
"error": "Invalid status filter"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Mark Conversation Read
Source: https://docs.sequenzy.com/api-reference/conversations/mark-read
POST /api/v1/conversations/{conversationId}/read
Mark all unread messages in a conversation as read
Mark all unread inbound messages in a conversation as read and clear the conversation's unread flag.
## Request
Conversation ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/read" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"updated": 2
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Conversation not found"
}
```
# Mark Conversation Unread
Source: https://docs.sequenzy.com/api-reference/conversations/mark-unread
POST /api/v1/conversations/{conversationId}/unread
Mark a conversation unread again
Set the conversation's unread flag and mark its latest received message unread, for example to leave it for a teammate. [Mark Conversation Read](./mark-read) or opening the conversation in the dashboard clears it again. Marking an already unread conversation is safe to repeat.
Requires the `conversations:write` scope.
## Request
Conversation ID.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/unread" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"conversation": {
"id": "conv_abc123",
"hasUnread": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Conversation not found",
"code": "conversation_not_found"
}
```
# Reply to Conversation
Source: https://docs.sequenzy.com/api-reference/conversations/reply
POST /api/v1/conversations/{conversationId}/messages
Send a reply or add an internal note
Send an email reply to the subscriber or add an internal team note. Replies are queued for delivery and reopen closed conversations. Notes are only visible to your team and do not reopen conversations.
## Request
Conversation ID.
Message type: `outbound` sends an email reply (default), `note` adds an
internal note.
Message subject. Defaults to the conversation subject.
Plain text body. Outbound messages require `bodyText` or `bodyHtml`.
HTML body. Outbound messages require `bodyText` or `bodyHtml`.
Sender profile to send an outbound reply from, for example the address the
subscriber originally wrote to. Its sending domain must be verified. Ignored
for notes. When omitted, the reply is sent from the API key owner's email
address, which must be on one of your verified sending domains. List profiles
with [List Sender Profiles](../sender-profiles/list).
Files to attach, up to 10 and 15 MB total after decoding. Each item has a
`filename`, base64 `content`, and an optional `contentType` (inferred from the
file name when omitted). Attachments need attachment storage on the server:
check `attachmentsAvailable` from [Get Inbox Address](./inbox-address).
Attachments on notes are stored but never emailed.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bodyText": "Happy to help! Let me know if you have other questions."}'
```
Reply with an attachment:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bodyText": "Invoice attached.", "attachments": [{"filename": "invoice.pdf", "content": "JVBERi0xLjQK..."}]}'
```
If a stored attachment cannot be read when the reply is sent, the reply is
retried and then marked `failed` rather than sent without the file.
Reply from a specific sender profile:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bodyText": "Happy to help!", "senderProfileId": "sp_abc123"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": {
"id": "msg_new123",
"conversationId": "conv_abc123",
"type": "outbound",
"subject": "Re: A quick update",
"bodyText": "Happy to help! Let me know if you have other questions.",
"bodyHtml": null,
"fromEmail": "support@example.com",
"fromName": null,
"fromUserId": "user_abc123",
"attachments": [],
"isRead": true,
"createdAt": "2026-06-11T10:00:00.000Z",
"deliveryStatus": "pending"
}
}
```
```json 400 theme={null}
{
"error": "Message body is required"
}
```
```json 400 (invalid attachment) theme={null}
{
"error": "Attachments can total at most 15 MB.",
"code": "invalid_attachment"
}
```
```json 503 (no attachment storage) theme={null}
{
"error": "Attachments are not available on this server.",
"code": "attachments_unavailable"
}
```
```json 400 (unverified sender) theme={null}
{
"error": "The sending domain for hello@pending.example.com is not verified. Verify the domain before replying from this address.",
"code": "sender_domain_not_verified"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Conversation not found"
}
```
```json 404 (unknown sender) theme={null}
{
"error": "Sender profile sp_abc123 was not found for this company.",
"code": "sender_profile_not_found"
}
```
```json 500 theme={null}
{
"error": "Failed to create message"
}
```
# Update Inbox Address
Source: https://docs.sequenzy.com/api-reference/conversations/update-inbox-address
PUT /api/v1/conversations/inbox-address
Use a branded inbox address on your own domain
Choose a branded inbox address such as `support@inbound.acme.com` on one of your verified sending domains, or clear it to use only the default address. The address uses the domain's inbound host, usually `inbound.{domain}` (older domains may use `inbound.` on their root domain), so your existing mailboxes on `acme.com` are not affected. `customAddress.address` in the response is the exact address.
The branded address starts receiving once the domain's inbound MX record is verified. Until then `customAddress.status` is `pending` and `address` stays the default `inbox+{companyId}@inbound.sequenzy.com`, which keeps working either way. The MX record is shown in **Settings → Inbox**, and [Verify Sending Domain](../websites/verify) rechecks it.
Changing or clearing an active branded address stops the old address immediately.
Requires the `companies:manage` scope, like other workspace settings. Reading the address with Get Inbox Address only needs `conversations:read`.
## Request
Name before the @, such as `support`. 1 to 64 letters, numbers, dots, dashes
or underscores, starting and ending with a letter or number. Stored in
lowercase. Some names such as `postmaster` and `abuse` are reserved. Send
`null` together with `domain: null` to clear the branded address.
One of your verified sending domains, such as `acme.com`. Send `null` together
with `localPart: null` to clear.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/conversations/inbox-address" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"localPart": "support", "domain": "acme.com"}'
```
Go back to the default address:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/conversations/inbox-address" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"localPart": null, "domain": null}'
```
The response has the same shape as [Get Inbox Address](./inbox-address).
## Responses
```json 200 theme={null}
{
"success": true,
"inbox": {
"enabled": true,
"address": "inbox+comp_abc123@inbound.sequenzy.com",
"defaultAddress": "inbox+comp_abc123@inbound.sequenzy.com",
"addresses": ["inbox+comp_abc123@inbound.sequenzy.com"],
"customAddress": {
"localPart": "support",
"domain": "acme.com",
"address": "support@inbound.acme.com",
"status": "pending"
},
"attachmentsAvailable": true,
"retentionDays": 90
}
}
```
```json 400 theme={null}
{
"error": "Use 1-64 letters, numbers, dots, dashes or underscores, starting and ending with a letter or number.",
"code": "invalid_local_part"
}
```
```json 404 theme={null}
{
"error": "Choose one of your verified sending domains.",
"code": "domain_not_found"
}
```
```json 409 theme={null}
{
"error": "support@inbound.acme.com is already used by another workspace.",
"code": "address_taken"
}
```
# Update Conversation Status
Source: https://docs.sequenzy.com/api-reference/conversations/update-status
POST /api/v1/conversations/{conversationId}/status
Open or close a conversation
Open or close a conversation. Closed conversations are reopened automatically when the subscriber replies or when you send a new outbound message.
## Request
Conversation ID.
New conversation status: `open` or `closed`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/conv_abc123/status" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "closed"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"conversation": {
"id": "conv_abc123",
"status": "closed"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Conversation not found"
}
```
```json 500 theme={null}
{
"error": "Failed to update conversation"
}
```
# Create Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/create
POST /api/v1/data-exports
Stream events and subscriber snapshots to your bucket
Adds a bucket as a data export destination. Before saving, Sequenzy writes a small test file to `/_sequenzy/connection-test.json`. If that write fails, nothing is saved and the response explains why. The first export starts immediately.
Requires `data_exports:write`, `subscribers:read` and `analytics:read`, because the destination receives subscriber and engagement data.
See [Data exports](/integrations/data-exports) for the file layout, row schemas and warehouse loading recipes.
## Request
Display name, up to 255 characters.
`s3` for Amazon S3 or any S3-compatible service, `gcs` for Google Cloud
Storage.
Bucket name, not a URL.
Bucket region, for example `us-east-1`. Required for Amazon S3 without a
custom endpoint. Defaults to `auto` for GCS and custom endpoints.
HTTPS origin of an S3-compatible service such as Cloudflare R2, MinIO or
Backblaze B2. It must resolve to a public address. Not allowed with `gcs`.
Folder inside the bucket. Leading and trailing slashes are removed. Letters,
numbers, `/` and `! _ . * ' ( ) = -` are allowed; `..` segments are rejected.
Access key ID. For GCS, the HMAC access ID.
Secret access key (GCS: HMAC secret). Stored encrypted and never returned.
Any of `email_events`, `sms_events`, `custom_events`, `subscribers`. Unknown
values return `422`.
`every_5_minutes`, `every_15_minutes`, `hourly` or `daily`.
ISO timestamp, at most 30 days ago, to include events recorded since then.
Defaults to now. The subscriber snapshot always reflects current data.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/data-exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Analytics lake",
"provider": "s3",
"bucket": "acme-exports",
"region": "us-east-1",
"pathPrefix": "sequenzy",
"accessKeyId": "AKIA...",
"secretAccessKey": "...",
"datasets": ["email_events", "custom_events", "subscribers"],
"frequency": "every_15_minutes"
}'
```
```json 201 theme={null}
{
"success": true,
"dataExport": {
"id": "exp_abc123",
"name": "Analytics lake",
"provider": "s3",
"bucket": "acme-exports",
"region": "us-east-1",
"endpoint": null,
"pathPrefix": "sequenzy",
"accessKeyId": "AKIA…WXYZ",
"datasets": ["email_events", "custom_events", "subscribers"],
"frequency": "every_15_minutes",
"isEnabled": true,
"status": "queued",
"startFrom": "2026-09-30T12:00:00.000Z",
"exportedThrough": {
"email_events": null,
"custom_events": null,
"subscribers": null
},
"nextRunAt": "2026-09-30T12:00:00.000Z",
"lastRunAt": null,
"lastSuccessAt": null,
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"totalRowsExported": 0,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "Access denied writing to \"acme-exports\". Grant the key permission to put objects under the configured path."
}
```
```json 400 theme={null}
{
"success": false,
"code": "invalid_input",
"error": "Region is required for Amazon S3 (for example us-east-1)."
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scopes: subscribers:read, analytics:read"
}
```
# Delete Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/delete
DELETE /api/v1/data-exports/{id}
Stop exporting to a bucket
Stops exporting to the destination and deletes its run history. Files already written stay in your bucket. If you recreate the export later, it starts from the time you recreate it. Requires `data_exports:delete`.
Data export ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/data-exports/exp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"dataExportId": "exp_abc123"
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# Get Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/get
GET /api/v1/data-exports/{id}
Get one data export with its progress
Returns one data export. The fields match [List Data Exports](/api-reference/data-exports/list). Requires `data_exports:read`.
Data export ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/data-exports/exp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"dataExport": {
"id": "exp_abc123",
"name": "Analytics lake",
"status": "failed",
"isEnabled": true,
"lastError": "Bucket \"acme-exports\" does not exist or is in another account.",
"consecutiveFailures": 2,
"nextRunAt": "2026-09-30T12:30:00.000Z"
}
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# List Data Exports
Source: https://docs.sequenzy.com/api-reference/data-exports/list
GET /api/v1/data-exports
List the buckets Sequenzy streams your data to
Returns every data export in the workspace with its status, how far each dataset has been exported, and the last error. Credentials are never returned; `accessKeyId` is masked.
Requires the `data_exports:read` scope. Marketers cannot use data export routes.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/data-exports" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`s3` or `gcs`.
`auto` for GCS and custom endpoints without a region.
Custom S3-compatible endpoint, or `null`.
Folder inside the bucket, without leading or trailing slashes.
Masked, for example `AKIA…WXYZ`.
Any of `email_events`, `sms_events`, `custom_events`, `subscribers`.
`every_5_minutes`, `every_15_minutes`, `hourly` or `daily`.
`false` while paused.
`idle`, `queued`, `running`, or `failed` (the last run failed and a retry
is scheduled).
Per dataset. Event datasets: everything recorded up to this timestamp has
been written. `subscribers`: date of the last complete snapshot. `null`
before the first export.
Readable reason for the last failure, cleared after a successful run.
Retries back off exponentially from the export frequency, up to 6 hours.
```json 200 theme={null}
{
"success": true,
"dataExports": [
{
"id": "exp_abc123",
"name": "Analytics lake",
"provider": "s3",
"bucket": "acme-exports",
"region": "us-east-1",
"endpoint": null,
"pathPrefix": "sequenzy",
"accessKeyId": "AKIA…WXYZ",
"datasets": ["email_events", "custom_events", "subscribers"],
"frequency": "every_15_minutes",
"isEnabled": true,
"status": "idle",
"startFrom": "2026-09-30T09:00:00.000Z",
"exportedThrough": {
"email_events": "2026-09-30T11:58:00.000Z",
"custom_events": "2026-09-30T11:58:00.000Z",
"subscribers": "2026-09-30"
},
"nextRunAt": "2026-09-30T12:13:00.000Z",
"lastRunAt": "2026-09-30T11:58:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:04.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"totalRowsExported": 184233,
"createdAt": "2026-09-30T09:00:00.000Z",
"updatedAt": "2026-09-30T12:00:04.000Z"
}
]
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: data_exports:read"
}
```
# Run Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/run
POST /api/v1/data-exports/{id}/run
Export now instead of waiting for the schedule
Queues an export immediately. Only one run per destination executes at a time; calling this while a run is queued or running does not start a second one. Poll [Get Data Export](/api-reference/data-exports/get) or [List Runs](/api-reference/data-exports/runs) for progress. Requires `data_exports:write`, `subscribers:read` and `analytics:read`.
Data export ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/data-exports/exp_abc123/run" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"queued": true,
"dataExport": {
"id": "exp_abc123",
"status": "queued"
}
}
```
```json 409 theme={null}
{
"success": false,
"code": "destination_disabled",
"error": "This destination is paused. Enable it before running an export."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# List Data Export Runs
Source: https://docs.sequenzy.com/api-reference/data-exports/runs
GET /api/v1/data-exports/{id}/runs
Recent export runs with row counts and errors
Lists runs from the last 14 days, newest first. A run that was interrupted (for example by a deploy) is marked `failed` and the next run resumes from the same point. Requires `data_exports:read`.
Data export ID.
Between 1 and 100.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/data-exports/exp_abc123/runs?limit=5" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"runs": [
{
"id": "run_xyz",
"destinationId": "exp_abc123",
"status": "succeeded",
"isManual": false,
"rowCount": 5120,
"fileCount": 2,
"byteCount": 402113,
"stats": {
"email_events": {
"rows": 5100,
"files": 1,
"bytes": 401002,
"windowFrom": "2026-09-30T11:43:00.000Z",
"windowTo": "2026-09-30T11:58:00.000Z"
},
"custom_events": {
"rows": 20,
"files": 1,
"bytes": 1111,
"windowFrom": "2026-09-30T11:43:00.000Z",
"windowTo": "2026-09-30T11:58:00.000Z"
},
"subscribers": { "rows": 0, "files": 0, "bytes": 0, "skipped": true }
},
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:04.000Z"
}
]
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# Test Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/test
POST /api/v1/data-exports/{id}/test
Check that the stored credentials can still write
Writes `/_sequenzy/connection-test.json` with the stored credentials. Use it after changing bucket policies or rotating keys on your side. Requires `data_exports:write`.
Data export ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/data-exports/exp_abc123/test" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"testedAt": "2026-09-30T12:00:00.000Z"
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "Bucket \"acme-exports\" is in a different region than the one configured."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# Update Data Export
Source: https://docs.sequenzy.com/api-reference/data-exports/update
PATCH /api/v1/data-exports/{id}
Change datasets, schedule, location or credentials
Updates a data export. Send only the fields you want to change. Requires `data_exports:write`, `subscribers:read` and `analytics:read`.
* Changing `bucket`, `region`, `endpoint`, `pathPrefix` or credentials writes a test file first. If it fails, nothing changes. Exports continue from their current progress in the new location.
* Datasets you add start from the time of the change. Datasets you remove stop immediately.
* Resuming a paused export (`isEnabled: true`) runs it at the next scheduler tick.
Data export ID.
Send `null` or `""` to clear a custom endpoint.
Must be sent together with `secretAccessKey`.
Must be sent together with `accessKeyId`.
`false` pauses the export, `true` resumes it.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/data-exports/exp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "frequency": "hourly", "datasets": ["email_events", "sms_events"] }'
```
```json 200 theme={null}
{
"success": true,
"dataExport": {
"id": "exp_abc123",
"frequency": "hourly",
"datasets": ["email_events", "sms_events"]
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "invalid_input",
"error": "Provide accessKeyId and secretAccessKey together when rotating credentials."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Data export destination not found."
}
```
# Clear Saved AI Style
Source: https://docs.sequenzy.com/api-reference/email-ai-style/clear
DELETE /api/v1/email-ai-style
Restore normal AI generation defaults without changing existing emails
Requires `emails:write`. [Get and review](./get) the current saved style, then pass its nonempty `revisionId` as `expectedStyleId`. Clearing restores normal defaults for future generation and keeps existing emails unchanged.
```bash theme={null}
curl -X DELETE https://api.sequenzy.com/api/v1/email-ai-style \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedStyleId":"STYLE_REVISION"}'
sequenzy email-ai-style clear --expected-style-id STYLE_REVISION --json
```
MCP `clear_email_ai_style` accepts `expectedStyleId` and optional `companyId`. Personal keys can use `X-Company-Id` or CLI `--company`.
```json theme={null}
{ "success": true, "style": null, "revisionId": null, "canManage": true }
```
An unsupported snapshot can also be cleared using the revision returned by GET. With no stored style, there is nothing to clear.
Errors: `401` for missing/invalid auth, `403` for missing scope or a non-writing role, and `422` for missing/empty revision. A stale or replayed clear returns `409 AI_STYLE_CONFLICT` and leaves the current state intact. Read and review the current style before retrying; clients do not retry conflicting writes automatically.
# Get Saved AI Style
Source: https://docs.sequenzy.com/api-reference/email-ai-style/get
GET /api/v1/email-ai-style
Inspect your company's saved AI email appearance and revision
Read your saved style before [replacing](./save) or [clearing](./clear) it. Requires `emails:read`.
```bash theme={null}
curl https://api.sequenzy.com/api/v1/email-ai-style \
-H "Authorization: Bearer YOUR_API_KEY"
```
The response includes `success`, `style`, `revisionId` and `canManage`. A style contains its `id`, `version`, source email ID/name, save timestamp, theme, font, layout preset and block appearance records. These records contain appearance, not source copy or links. Styles saved with layout habits also carry `layout.outline` (the source section order) and `layout.rules` (confirmed habits with stable IDs such as `around|button|divider:dots`), plus optional `notes`. Pass rule IDs to [save](./save) as `layoutRuleIds` to keep a subset.
With no saved style, `style` and `revisionId` are `null`. If a stored version is unsupported, `style` is `null` but `revisionId` still identifies it so you can replace or clear it. Do not treat that state as an initial save.
```json theme={null}
{ "success": true, "style": null, "revisionId": null, "canManage": true }
```
Use `sequenzy email-ai-style get --json` or MCP `get_email_ai_style`. Personal keys can select a company using `X-Company-Id`, CLI `--company`, or MCP `companyId`.
Errors: `401` for invalid/missing auth, `403` for missing scope or denied company access, and `404` if the selected company no longer exists. `canManage: false` means you can inspect this style but cannot change it.
# Save AI Style
Source: https://docs.sequenzy.com/api-reference/email-ai-style/save
PUT /api/v1/email-ai-style
Capture an email's appearance for your future AI-generated emails
Save an independent appearance snapshot from an email you can access. Requires `emails:write`. Marketers can save marketing email styles; transactional sources remain protected. Existing emails, source content and your company theme stay unchanged.
[Get the current style](./get) first. Pass its `revisionId` as `expectedStyleId`; use `null` only when no style is stored.
```bash theme={null}
curl -X PUT https://api.sequenzy.com/api/v1/email-ai-style \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emailId":"email_123","expectedStyleId":null}'
```
`emailId` is the underlying email ID, including a campaign, sequence or transactional email's content row. It is not a campaign ID or transactional slug. The API captures the stored blocks and resolves the email's theme/font overrides before company defaults. Subsequent source edits or deletion do not change your saved style.
To capture an unsaved design, provide `canvas` with all four fields: `blocks`, `theme`, `fontFamily` and `emailPreset` (`branded` or `minimal`). `theme` is a complete editor theme with `presetId`, `colors`, `typography` and `layout`; use the schema above for required fields. Canvas validation is shared with the editor. Blocks are limited to 500 and 500,000 serialized characters. Empty content or footer/logo scaffolding cannot define a style. Imported HTML supports inline typography and colors, not an exact stylesheet/layout clone.
## Layout habits and notes
Saving also detects layout habits in the source: the block it opens with (for example a video) and content-free companions that consistently frame a block type (for example a dotted divider above and below every button). Every detected habit is kept unless you narrow them. The response lists them under `style.layout.rules`, each with a stable `id` such as `around|button|divider:dots`, and `style.layout.outline` records the source section order.
* `layoutRuleIds`: IDs of the habits to keep. Omit to keep all; pass `[]` to keep none. Unknown IDs are ignored.
* `notes`: up to 500 characters of design guidance for future generations, such as "always open with a short video". Notes are never treated as email content.
Generation follows the confirmed habits in its prompt and inserts the captured dividers or spacers deterministically around matching blocks. Opening habits that need content, such as a video, are prompt guidance only. To review before committing, save once, inspect `style.layout.rules`, then save again with the subset you want and the new `revisionId`.
```bash theme={null}
curl -X PUT https://api.sequenzy.com/api/v1/email-ai-style \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emailId":"email_123","expectedStyleId":"STYLE_REVISION","layoutRuleIds":["around|button|divider:dots"],"notes":"Keep the dotted dividers around buttons"}'
```
```bash theme={null}
sequenzy email-ai-style save email_123 --if-unset
sequenzy email-ai-style save email_123 --expected-style-id STYLE_REVISION --canvas-file ./canvas.json --json
sequenzy email-ai-style save email_123 --expected-style-id STYLE_REVISION --layout-rules "around|button|divider:dots" --notes "Keep the dotted dividers around buttons"
sequenzy email-ai-style save email_123 --expected-style-id STYLE_REVISION --without-layout-rules
```
MCP `save_email_ai_style` accepts `emailId`, `expectedStyleId`, optional `canvas`, optional `layoutRuleIds`, optional `notes` and optional `companyId`. CLI `--company` selects the company for a personal key.
The response returns `success`, the new `style`, its new `revisionId` and `canManage`. New AI generation across dashboard/API/CLI/MCP uses this appearance and its confirmed habits. Explicit style instructions and plain-text choices take precedence. See [Email Themes](../../concepts/email-themes).
Errors: `400` for invalid/oversized canvas, empty content or notes over 500 characters; `401` for missing/invalid auth; `403` for missing scope, denied company access or a non-writing role; `404` for an inaccessible/missing source; `422` for missing or malformed required fields. `409 AI_STYLE_CONFLICT` means another write won: get the current style, review it and explicitly retry using its revision. Never retry with a newly fetched revision automatically.
# Preview Cart Items
Source: https://docs.sequenzy.com/api-reference/email-blocks/assist-cart
POST /api/v1/email-blocks/line-items/assist
Suggest cart field mappings and create isolated preview scenarios from supplied items.
You can inspect your item fields and test cart layouts without saving an email or changing recipient data. This endpoint requires a valid API key, accepts existing restricted keys, and reads only the data you supply.
Up to 50 item objects. Defaults to an empty array. Fetch an event sample
separately if you want real event data.
Optional existing mappings for title, variantTitle, quantity, priceCents,
price, imageUrl, url and currency. Values are dotted paths within one item, up
to 200 characters. Blank values use default names. Existing mappings are never
replaced by suggestions. Unsafe prototype paths are rejected.
Defaults to real, which clones supplied items unchanged. Choose one, many (12
rows), empty, missing-images or long-names. Nonempty sample scenarios create a
sample product when no items were supplied. Rendered row count still follows
your block's maxItems setting.
minor (default) interprets numeric unit prices as cents; major interprets
29.99 as 29.99. Choose units explicitly for ambiguous numeric fields. This
never changes the separate total, which stays in cents.
```bash theme={null}
curl https://api.sequenzy.com/api/v1/email-blocks/line-items/assist \
-H 'Authorization: Bearer API_KEY' \
-H 'Content-Type: application/json' \
-d '{"items":[{"name":"Candle","amount":29.99}],"scenario":"real"}'
```
A successful response includes `success: true`, `suggestions` (field, path, example), `priceCandidates` (path, example, units: unknown), cloned `previewItems`, `scenario`, `isSample`, and three design `presets` (id, label, description, settings). Suggestions require explicit application. Unknown numeric price units are never inferred. `isSample` is true for every scenario except real, including empty.
Use `sequenzy blocks preview-cart --items-file items.json --scenario many --json`, or MCP `preview_cart_items` with the same input fields. CLI uses `--fields` for the `itemFields` JSON object and `--price-unit` for `priceUnit`. Human output also prints mapping suggestions, unit choices, presets and preview items.
To preview HTML, supply the returned items at your block's source in the existing render endpoint's `variables`, and view its HTML at desktop or mobile width. To save, apply chosen mappings, price units and preset settings through the existing campaign, sequence email, template or transactional block update workflow. Read the current block first and preserve unrelated fields. To clear optional configuration, remove those properties from the replacement block; null is not a valid style, mapping or unit value. This assistance endpoint never saves or sends anything, so repeating a request has no persistent side effects. See [Cart and Order Blocks](/concepts/cart-and-order-blocks).
Malformed item arrays, more than 50 items, unsafe mappings, invalid scenarios, null fields or unsupported units return **400** with `success: false`, an `error` and validation `details`. Missing or invalid authentication returns **401**; normal authentication restrictions may return **403**. There is no event lookup or not-found response because all item data is supplied in the request. Transport failures can be retried with the same input.
# Get Email Block Type
Source: https://docs.sequenzy.com/api-reference/email-blocks/get
GET /api/v1/email-blocks/{type}
Get the full field reference for one email block type.
You can read the complete field reference for a single block type, including a minimal valid example and authoring notes.
Each entry in `fields` describes one field. An array field carries `itemFields` with the shape of one entry, and an object field carries its own `fields` - so `GET /api/v1/email-blocks/repeat` returns the `mode`, `strategy`, `lookbackDays`, `sort`, and `filters` a `productSource` accepts, with the allowed values of each.
The response carries `conditionFields`, the per-field table for block conditions, when you request `conditional-group` or pass `?conditionFields=true`. Any other type returns `conditionFieldsHint` instead, because the table is several times the size of one block type's reference and a targeted lookup should stay small. See [List Email Block Types](/api-reference/email-blocks/list#block-conditions).
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-blocks/text?conditionFields=true" \
-H "Authorization: Bearer API_KEY"
```
For `line-items`, the optional `itemFields` object maps `title`, `variantTitle`, `quantity`, `priceCents`, `price`, `imageUrl`, `url`, and `currency` to dotted paths relative to one item. For example, `{"itemFields":{"title":"product.name","quantity":"count"}}`. Omitted or blank entries use the standard field name. Paths accept up to 200 characters; non-string or longer mappings fail block validation. Numeric `priceCents` uses cents by default; set `priceUnit: "major"` explicitly for major-unit values; `price` is a formatted fallback. The CLI `sequenzy blocks line-items` and MCP `get_email_block_schema` return this same field reference. See [Cart and Order Blocks](/concepts/cart-and-order-blocks#choose-your-data-and-item-fields).
## Request
Block type, for example `list`, `steps`, `text`, or `hero`.
Pass `true` to include the per-field condition table. Not needed for
`conditional-group`, which always carries it.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-blocks/steps?conditionFields=true" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"blockType": {
"type": "steps",
"creatable": true,
"required": ["items", "variant"],
"optional": ["id", "accentColor", "styles", "conditions"],
"fields": [
{
"name": "items",
"required": true,
"type": "array",
"itemFields": [
{ "name": "id", "required": false, "type": "string" },
{ "name": "title", "required": true, "type": "string" },
{ "name": "description", "required": false, "type": "string" }
]
},
{
"name": "variant",
"required": true,
"type": "enum",
"values": ["numbered", "timeline"]
},
{ "name": "accentColor", "required": false, "type": "string" }
],
"example": {
"type": "steps",
"variant": "timeline",
"items": [
{ "title": "Connect", "description": "Link your account." },
{ "title": "Launch", "description": "Send your first campaign." }
]
}
},
"conditionFields": [
{
"field": "tag",
"label": "Tag",
"operators": ["contains", "not_contains", "is_empty", "is_not_empty"],
"valueFormat": "One tag name, such as \"extended\".",
"serverEvaluated": true,
"previewSupport": "inline_tags_or_stored_subscriber",
"example": {
"id": "c1",
"field": "tag",
"operator": "contains",
"value": "extended"
}
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Unknown block type: numbered-list. Valid block types: text, html, heading, list, button, ..."
}
```
## Related
* [List Email Block Types](/api-reference/email-blocks/list)
* [Visual Blocks](/concepts/visual-blocks)
Cart design fields include `imageSize`, `imageFit`, `rowSpacing`, `fontSize` and `showPrices`. [Preview Cart Items](/api-reference/email-blocks/assist-cart) returns presets, mapping suggestions and scenario data without saving.
# List Email Block Types
Source: https://docs.sequenzy.com/api-reference/email-blocks/list
GET /api/v1/email-blocks
List every email block type with its required and optional fields.
You can list every block type accepted by the `blocks` array on campaigns, sequence email steps, templates, transactional emails, and email components. Each entry names the required and optional fields, the allowed values of every enum field, and the shape of nested item arrays and nested objects.
This reference is derived from the same schemas that validate a write, so it cannot drift from what those endpoints accept. The MCP tool `get_email_block_schema` and the `sequenzy://email-blocks` resource return the same payload; if the tool is missing from a client's tool list, enable it on the Sequenzy connector or call this endpoint instead of inferring fields from live HTML.
## Request
Pass `true` to hide structural block types the editor manages for you, such as
`group`, `conditional-group`, and `footer`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-blocks?creatableOnly=true" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"blockTypes": [
{
"type": "list",
"creatable": true,
"required": ["variant", "items"],
"optional": ["id", "color", "styles", "conditions"],
"fields": [
{
"name": "variant",
"required": true,
"type": "enum",
"values": ["bullet", "numbered", "checklist"]
},
{
"name": "items",
"required": true,
"type": "array",
"itemFields": [
{ "name": "id", "required": false, "type": "string" },
{ "name": "content", "required": true, "type": "string" },
{ "name": "color", "required": false, "type": "string" }
]
}
],
"example": {
"type": "list",
"variant": "bullet",
"items": [{ "content": "First point" }, { "content": "Second point" }]
},
"notes": ["Variants: \"bullet\", \"numbered\", or \"checklist\"."]
}
],
"conditionFields": [
{
"field": "tag",
"label": "Tag",
"operators": ["contains", "not_contains", "is_empty", "is_not_empty"],
"valueFormat": "One tag name, such as \"extended\".",
"serverEvaluated": true,
"previewSupport": "inline_tags_or_stored_subscriber",
"example": {
"id": "c1",
"field": "tag",
"operator": "contains",
"value": "extended"
}
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
## Authoring notes
Lists are their own block type rather than a text variant. A `text` block accepts only `variant: "paragraph"`, `"lead"`, or `"html"`, and never accepts `items`.
```json theme={null}
{ "type": "list", "variant": "numbered", "items": [{ "content": "First" }] }
{ "type": "steps", "variant": "numbered", "items": [{ "title": "Step one", "description": "What happens" }, { "title": "Step two", "description": "What happens next" }] }
```
Use `list` for a plain numbered or bulleted list, and `steps` for a visual numbered walkthrough. List items carry `content`; steps items carry `title` and an optional `description`.
## Block conditions
Every block takes a `conditions` array that hides it unless the contact matches, and `conditional-group` wraps an if/else pair around the same filters. Inside a block reference, `field` and `operator` are two flat enums that pool every field's operators together - the validator narrows them against each other, so `{ "field": "tag", "operator": "is" }` parses as an enum member and is then rejected.
`conditionFields` is that narrowing, spelled out. This endpoint always returns it; a single-type lookup returns it for `conditional-group` or with `?conditionFields=true`, and otherwise returns a `conditionFieldsHint` string in its place:
```json theme={null}
{
"field": "tag",
"label": "Tag",
"operators": ["contains", "not_contains", "is_empty", "is_not_empty"],
"valueFormat": "One tag name, such as \"extended\". A comma-separated list matches any of them for `contains`, and none of them for `not_contains`.",
"serverEvaluated": true,
"previewSupport": "inline_tags_or_stored_subscriber",
"example": {
"id": "c1",
"field": "tag",
"operator": "contains",
"value": "extended"
}
}
```
`serverEvaluated` marks the fields read from stored subscriber state rather than from the merge data of the send, and `previewSupport` says what a [render](/api-reference/campaigns/render) needs before it can evaluate one:
* `any_contact` - resolved from merge data, so any contact previews it.
* `inline_tags_or_stored_subscriber` - `tag`, which an inline contact can state for itself via `subscriber.tags`.
* `stored_subscriber` - needs `subscriberId`; nothing inline stands in for it.
A condition a render cannot evaluate is not an error: it renders as `false` and is listed in that render's `unevaluatedConditions`.
## Related
* [Get Email Block Type](/api-reference/email-blocks/get)
* [Visual Blocks](/concepts/visual-blocks)
# Create Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/create
POST /api/v1/email-components
Create a reusable email component from a block list.
You can save a block group as a reusable component. Component names are unique per company.
Creating a component with `componentType: "footer"` does not pin it as the
company default footer. Use [Set Default Email
Component](/api-reference/email-components/set-default) for that.
## Request
Component name, unique within the company.
Blocks that make up the component. Must contain at least one block.
Optional human-readable description.
`section` (default) or `footer`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/email-components" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Promo banner","blocks":[{"type":"text","content":"
" }],
"componentType": "section",
"defaultSlot": null,
"version": 1,
"createdAt": "2026-08-12T11:00:00.000Z",
"updatedAt": "2026-08-12T11:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "At least one block is required"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 409 theme={null}
{
"success": false,
"error": "A component with this name already exists"
}
```
# Delete Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/delete
DELETE /api/v1/email-components/{componentId}
Delete an email component.
You can delete a component. Emails that already rendered it keep their copied blocks, so deleting does not change existing emails.
Deleting the component pinned as the default footer makes new emails fall back to the footer Sequenzy generates from your company profile.
## Request
Email component ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/email-components/cmp_123" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"id": "cmp_123"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Component not found"
}
```
# Get Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/get
GET /api/v1/email-components/{componentId}
Get one email component's blocks and metadata.
You can fetch a single component by id, including its `version` and whether it is pinned as a company default slot.
## Request
Email component ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-components/cmp_123" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"component": {
"id": "cmp_123",
"companyId": "comp_123",
"name": "Promo banner",
"description": "Seasonal banner",
"blocks": [{ "id": "block_1", "type": "text", "content": "
Sale
" }],
"componentType": "section",
"defaultSlot": null,
"version": 1,
"createdAt": "2026-01-10T10:00:00.000Z",
"updatedAt": "2026-01-10T10:00:00.000Z"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Component not found"
}
```
# Get Default Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/get-default
GET /api/v1/email-components/defaults/{slot}
Read the company's default component for a slot, such as the default email footer.
You can read the component pinned as your company default for a slot. The default `footer` component is the footer that new sequence, campaign, and AI-generated emails are built with, so this is what you read before changing the footer everywhere.
When no default is set, the endpoint returns `404` and emails fall back to the footer Sequenzy generates from your company profile.
## Request
Default slot to read. Currently only `footer`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-components/defaults/footer" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"component": {
"id": "cmp_123",
"companyId": "comp_123",
"name": "Default Footer",
"description": null,
"blocks": [
{
"id": "block_1",
"type": "footer",
"variant": "full",
"showUnsubscribe": true
}
],
"componentType": "footer",
"defaultSlot": "footer",
"version": 3,
"createdAt": "2026-01-10T10:00:00.000Z",
"updatedAt": "2026-08-01T09:30:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unsupported slot: header. Supported slots: footer"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "No default footer component is set for this company. Emails fall back to the generated footer."
}
```
## Related
* [Set Default Email Component](/api-reference/email-components/set-default)
* [List Email Components](/api-reference/email-components/list)
# List Email Components
Source: https://docs.sequenzy.com/api-reference/email-components/list
GET /api/v1/email-components
List reusable email components, including the components pinned as company defaults.
You can list the company's reusable email components, newest first. Components are saved block groups you can reuse across emails, plus the components pinned as a company default for a slot.
## Request
Filter by component type: `section` or `footer`.
Filter by default slot. Currently only `footer`.
Pass `true` to return only components pinned as a company default.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-components?defaultsOnly=true" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"components": [
{
"id": "cmp_123",
"companyId": "comp_123",
"name": "Default Footer",
"description": null,
"blocks": [{ "id": "block_1", "type": "footer", "variant": "full" }],
"componentType": "footer",
"defaultSlot": "footer",
"version": 3,
"createdAt": "2026-01-10T10:00:00.000Z",
"updatedAt": "2026-08-01T09:30:00.000Z"
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unsupported slot: header. Supported slots: footer"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
## Related
* [Get Default Email Component](/api-reference/email-components/get-default)
# Preview Default Footer
Source: https://docs.sequenzy.com/api-reference/email-components/preview-default
POST /api/v1/email-components/defaults/{slot}/preview
Review affected emails and rendered HTML before applying your default footer.
You can preview a footer update without saving it. Send `blocks` (a nonempty email
block array), optional `name` and nullable `description`, and required `application`
with `scopes` and optional `includeCustomized`. An empty scope array previews only
the default. The supported slot is `footer`.
```json theme={null}
{
"blocks": [
{
"type": "footer",
"variant": "full",
"companyName": "Acme",
"address": "123 Main Street"
}
],
"application": {
"scopes": ["sequences", "campaigns"],
"includeCustomized": false
},
"renderPreview": true
}
```
The `200` response includes `success`, `application.token`, `application.counts`,
`application.affected`, and `application.skipped`. Each item has `scope`, `kind`,
`id`, and `name`; skipped items also have `reason`. With `renderPreview: true`,
`renderedPreview` contains `footerHtml`, `samples` with `beforeHtml` and `afterHtml`,
and a preview limitation `note`. By default you receive up to one sample per selected
category. Supply `sample: { id, kind }` for a particular affected email version.
Personal keys require workspace admin access. Keys need `emails:write`, selected
category write scopes, and `ab_tests:write` for sequences or campaigns, matching
application permissions. Invalid blocks or slots return `400`, invalid credentials
return `401`, missing permissions return `403`.
Malformed request shapes, such as missing required fields or invalid scope values,
return `422`. Unexpected rendering or database failures return `500`; transient database
unavailability may return `503`.
Apply with [Set Default Email Component](/api-reference/email-components/set-default)
using identical inputs and `previewToken`. Preview does not modify content or send emails.
# Set Default Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/set-default
PUT /api/v1/email-components/defaults/{slot}
Create or replace the company's default component for a slot, such as the email footer.
You can change the footer every new email gets by writing the company's default `footer` component. Sequence, campaign, and AI-generated emails clone this component when they are built, instead of the footer Sequenzy generates from your company profile.
This replaces the whole component. Read the current one with [Get Default Email Component](/api-reference/email-components/get-default) first when you only mean to change part of it.
A default footer always keeps its unsubscribe link enabled. Transactional
sends hide it at render time. Without application options, existing emails
keep their saved footer. Use [Preview Default
Footer](/api-reference/email-components/preview-default) before applying to
existing emails.
## Request
Default slot to write. Currently only `footer`.
Blocks that make up the component. Must contain at least one block. Put block
styling under `styles`; top-level style keys are normalized into `styles`.
Component name. Defaults to `Default Footer` when creating the footer default.
Names are unique per company.
Optional human-readable description.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/email-components/defaults/footer" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"blocks":[{"type":"footer","variant":"full","companyName":"Acme","address":"123 Main Street"}]}'
```
Optional selected `scopes` and `includeCustomized` boolean, matching your
preview. Scopes accept `sequences`, `campaigns`, `transactional`, and
`templates`.
Required with application scopes. Use the token returned by preview with
identical blocks, metadata, and application options. Stale previews return
`409` without writes.
Application requires admin access for personal keys, `emails:write`, each selected
category's write scope, and `ab_tests:write` for sequences or campaigns. Missing
permissions return `403`. Invalid request shapes return `422`. Success also returns `application` with counts, affected
items and skipped reasons. Unexpected failures return `500` with no partial application.
## Responses
```json 200 theme={null}
{
"success": true,
"component": {
"id": "cmp_123",
"companyId": "comp_123",
"name": "Default Footer",
"description": null,
"blocks": [
{
"id": "block_1",
"type": "footer",
"variant": "full",
"companyName": "Acme",
"showUnsubscribe": true
}
],
"componentType": "footer",
"defaultSlot": "footer",
"version": 4,
"createdAt": "2026-01-10T10:00:00.000Z",
"updatedAt": "2026-08-12T11:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "emailComponents.saveDefault: invalid block at 0: Unknown block type"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 409 theme={null}
{
"success": false,
"error": "A component with this name already exists"
}
```
## Related
* [Get Default Email Component](/api-reference/email-components/get-default)
* [Update Email Component](/api-reference/email-components/update)
# Update Email Component
Source: https://docs.sequenzy.com/api-reference/email-components/update
PATCH /api/v1/email-components/{componentId}
Update an email component's metadata or replace its blocks.
You can rename a component, change its description or type, or replace its blocks. Omitted fields are preserved.
Replacing `blocks` bumps the component `version`. Emails that already cloned the component keep the copy they were built with; the new version applies to emails built afterwards.
Editing the component pinned as the default footer keeps its unsubscribe link
enabled, the same as writing it through the defaults route.
## Request
Email component ID.
New component name, unique within the company.
New description. Send an empty string to clear it.
Replacement blocks. Replaces the whole block list and must contain at least
one block.
`section` or `footer`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/email-components/cmp_123" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Promo banner v2"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"component": {
"id": "cmp_123",
"companyId": "comp_123",
"name": "Promo banner v2",
"description": null,
"blocks": [{ "id": "block_1", "type": "text", "content": "
Sale
" }],
"componentType": "section",
"defaultSlot": null,
"version": 2,
"createdAt": "2026-01-10T10:00:00.000Z",
"updatedAt": "2026-08-12T11:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "No supported fields provided"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Component not found"
}
```
```json 409 theme={null}
{
"success": false,
"error": "A component with this name already exists"
}
```
# Get Email Design System
Source: https://docs.sequenzy.com/api-reference/email-design-system/get
GET /api/v1/email-design-system
Get the visual identity every AI-generated email renders inside
Get the company's effective email design system - the locked visual grammar (kicker style, title alignment, button shape, divider style, density, sanctioned opener treatments) and the composition spine that anchors how AI-generated emails and sequence steps are structured.
The identity is stored as the company's `emailDesignPrompt` design direction text: tokens are parsed from that text, and any token the text does not state is derived deterministically from brand context. While the text is empty or untouched, `isDefault` is `true`. Adjust tokens with [Update Email Design System](/api-reference/email-design-system/update), or edit the text as free prose via `PATCH /companies/:companyId`.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-design-system" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
How eyebrows above titles render: `chip` (soft badge chips), `letterspaced`
(uppercase letter-spaced text), or `none`.
`left` or `center` alignment for headings and hero content.
`rounded` rectangles or fully rounded `pill` buttons.
`line` (thin dividers between sections) or `space` (whitespace only).
`airy` (generous spacing) or `compact` (tight sections).
The opener treatments this company's emails may use, primary first. Values:
`hero-image`, `hero-color`, `header-hero`, `editorial-masthead`,
`title-led`.
Which worked-example skeleton anchors generation: `hero-led` (designed
marketing sections), `editorial` (typography-first), or `product-spec`
(information-dense).
`derived` when every token comes from brand derivation; `custom` when the
direction text carries the identity.
The raw design direction text the tokens were parsed from; `null` when the
company has none yet.
True while the identity is purely derived from brand context.
## Responses
```json 200 theme={null}
{
"success": true,
"designSystem": {
"version": 1,
"designCode": {
"version": 1,
"kickerStyle": "letterspaced",
"titleAlignment": "center",
"buttonShape": "pill",
"dividerStyle": "line",
"density": "airy",
"openerTreatments": ["editorial-masthead", "hero-color"]
},
"compositionSpine": "editorial",
"source": "derived"
},
"directionText": null,
"isDefault": true
}
```
```json 401 theme={null}
{"success": false, "error": {"code": "UNAUTHORIZED", "message": "Invalid API key"}}
```
```json 404 theme={null}
{"success": false, "error": {"code": "NOT_FOUND", "message": "Company not found"}}
```
# Update Email Design System
Source: https://docs.sequenzy.com/api-reference/email-design-system/update
PATCH /api/v1/email-design-system
Adjust the visual identity every AI-generated email renders inside
Adjust the company's email design system. This is a partial update - only the fields you pass change - and it applies to every future AI email generation and sequence enrichment.
The adjustment is persisted into the company's `emailDesignPrompt` design direction text - the single source of truth: the new identity's sentences are prepended so they win any later parse, and custom prose the text carried is preserved below them. For free-prose edits, use `PATCH /companies/:companyId` with `emailDesignPrompt` instead.
## Request
Partial visual-grammar adjustment. Fields: `kickerStyle` (`chip` |
`letterspaced` | `none`), `titleAlignment` (`left` | `center`),
`buttonShape` (`rounded` | `pill`), `dividerStyle` (`line` | `space`),
`density` (`airy` | `compact`), `openerTreatments` (array drawn from
`hero-image`, `hero-color`, `header-hero`, `editorial-masthead`,
`title-led`).
Which worked-example skeleton anchors generation: `hero-led`, `editorial`,
or `product-spec`.
`true` clears the direction text and returns to the brand-derived
defaults; the next generation prefills the text again. Cannot be combined
with other fields.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/email-design-system" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"compositionSpine": "editorial",
"designCode": {
"kickerStyle": "letterspaced",
"openerTreatments": ["editorial-masthead", "title-led"]
}
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Updated the email design system and rewrote the design direction text to match.",
"designSystem": {
"version": 1,
"designCode": {
"version": 1,
"kickerStyle": "letterspaced",
"titleAlignment": "center",
"buttonShape": "pill",
"dividerStyle": "line",
"density": "airy",
"openerTreatments": ["editorial-masthead", "title-led"]
},
"compositionSpine": "editorial",
"source": "custom"
},
"directionText": "Short uppercase letter-spaced kickers above section titles. Centered headings and hero content. Fully rounded pill buttons. Thin divider lines between sections. Generous spacing: one idea per section with room to breathe. Open emails with an editorial masthead (kicker, display title, short standfirst, thin divider) or a plain typography-first opening (kicker, title, short lead text). Compose emails as typography-first editorial pieces: display headings, hairline rules, and quiet actions.",
"isDefault": false
}
```
```json 400 theme={null}
{"success": false, "error": {"code": "BAD_REQUEST", "message": "Invalid email design system: unrecognized design code value"}}
```
```json 401 theme={null}
{"success": false, "error": {"code": "UNAUTHORIZED", "message": "Invalid API key"}}
```
```json 404 theme={null}
{"success": false, "error": {"code": "NOT_FOUND", "message": "Company not found"}}
```
```json 409 theme={null}
{"success": false, "error": {"code": "CONFLICT", "message": "The email design system was modified concurrently; retry the update."}}
```
# Get Email Send
Source: https://docs.sequenzy.com/api-reference/email-sends/get
GET /api/v1/email-sends/{emailSendId}
Get an email delivery status, failure reason, HTML body, and event timeline by emailSendId.
You can retrieve a queued, sent, suppressed, or failed email by `emailSendId`. Email send rows are cleaned up after 14 days; when the row is gone, the API returns retained ClickHouse events and a sparse summary without `emailBody`. Test sends appear in sent-email history and are available through this exact-ID endpoint while their row is retained.
Use `status` and `errorMessage` as the delivery contract. Queue jobs are an internal execution detail and are not exposed by this API.
`status: "failed"` is not a bounce. When `internalFailureCode` is
`transport_exhausted`, the recipient provider refused every delivery route we
had; when it is `admin_bounce`, an operator removed the message from the
delivery queue; when it is `crash_orphaned_claim`, the sending worker stopped
between claiming the send and recording a transport outcome, so we never
confirmed the message reached the provider. In every case the address is still
valid, remains subscribed, is not added to the suppression list, and does not
count toward your bounce rate. Only `status: "bounced"` means the provider
judged the mailbox itself. New failure codes can be added over time, so treat
an unrecognized `internalFailureCode` as a generic failure.
`type` identifies the send source (`campaign`, `sequence`, or `transactional`)
for compatibility with analytics. `emailType` is the normalized delivery policy
(`marketing` or `transactional`), and `isTransactional` is its stored snapshot.
For example, a marketing message sent through `send_email` has
`type: "transactional"`, `emailType: "marketing"`, and
`isTransactional: false`. Timeline events expose the same policy as
`deliveryPolicy`; their legacy `emailType` field remains the source category.
For cleaned-up legacy sequence/API sends without a stored policy snapshot,
`emailType`, `isTransactional`, and event `deliveryPolicy` are `null` rather
than guessed.
For multi-recipient transactional sends (multiple `to` addresses or any `cc`/`bcc`), `additionalRecipients` contains the full recipient lists as actually sent. It is `null` for single-recipient sends and for ClickHouse-only fallbacks.
CC/BCC delivery records return `isCopiedRecipient: true` and identify the original delivery with `primaryEmailSendId` when it is still available. Their `emailBody` is `null` because content, opens, and clicks remain attached to the primary email send.
## Request
Email send ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-sends/send_123" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"source": "database",
"emailSend": {
"id": "send_123",
"companyId": "company_123",
"type": "transactional",
"emailType": "marketing",
"isTransactional": false,
"recipientEmail": "user@example.com",
"additionalRecipients": null,
"subscriberExternalId": "customer_123",
"subject": "Welcome",
"senderEmail": "hello@example.com",
"senderName": "Acme",
"originalReplyTo": "Support ",
"status": "delivered",
"suppressionReason": null,
"emailBody": "
Welcome
",
"isCopiedRecipient": false,
"primaryEmailSendId": null,
"sentAt": "2026-05-01T12:00:00.000Z",
"deliveredAt": "2026-05-01T12:00:03.000Z",
"suppressedAt": null,
"createdAt": "2026-05-01T11:59:58.000Z"
},
"events": [
{
"id": "evt_123",
"emailSendId": "send_123",
"emailType": "transactional",
"deliveryPolicy": "marketing",
"eventType": "delivery",
"eventTime": "2026-05-01T12:00:03.000Z"
}
]
}
```
```json 200 theme={null}
{
"success": true,
"source": "database",
"emailSend": {
"id": "send_copy_123",
"companyId": "company_123",
"type": "transactional",
"recipientEmail": "archive@example.com",
"subject": "Welcome",
"status": "delivered",
"emailBody": null,
"isCopiedRecipient": true,
"primaryEmailSendId": "send_123",
"sentAt": "2026-05-01T12:00:00.000Z",
"deliveredAt": "2026-05-01T12:00:03.000Z",
"createdAt": "2026-05-01T11:59:58.000Z"
},
"events": []
}
```
```json 200 theme={null}
{
"success": true,
"source": "clickhouse_events",
"message": "Email send row was not found; returned retained ClickHouse event data. The HTML body is unavailable after email send cleanup.",
"emailSend": {
"id": "send_123",
"companyId": "company_123",
"type": "campaign",
"recipientEmail": "user@example.com",
"subject": "Spring launch",
"status": "clicked",
"emailBody": null,
"sentAt": "2026-04-01T10:00:00.000Z",
"clickedAt": "2026-04-01T10:05:00.000Z",
"createdAt": "2026-04-01T10:00:00.000Z"
},
"events": [
{
"id": "evt_send",
"emailSendId": "send_123",
"eventType": "send",
"eventTime": "2026-04-01T10:00:00.000Z"
},
{
"id": "evt_click",
"emailSendId": "send_123",
"eventType": "click",
"clickedUrl": "https://example.com",
"eventTime": "2026-04-01T10:05:00.000Z"
}
]
}
```
```json 200 theme={null}
{
"success": true,
"source": "database",
"emailSend": {
"id": "send_suppressed",
"companyId": "company_123",
"type": "campaign",
"recipientEmail": "bounced@example.com",
"subject": "Spring launch",
"status": "suppressed",
"suppressionReason": "bounced",
"errorMessage": "Email suppressed due to previous bounce (bounced_email)",
"sentAt": null,
"suppressedAt": "2026-05-01T12:00:00.000Z",
"createdAt": "2026-05-01T11:59:58.000Z"
},
"events": []
}
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 404 theme={null}
{ "success": false, "error": "Email send not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# List Sent Emails
Source: https://docs.sequenzy.com/api-reference/email-sends/list
GET /api/v1/email-sends
Search and filter the recent sent-email delivery history by subject, recipient, status, type, and source.
Returns the same 14-day delivery history used by the dashboard, including pending,
successful, and failed test sends. Test rows have `isTestEmail: true`. Filter this
field in your application if you want live deliveries only; `isTestEmail` is not
a query parameter. CC/BCC bookkeeping rows remain hidden, except bounced or
complained copied recipients that carry sender-health feedback.
`search` (or `q`) searches both subject/title and recipient email. Use
`subject` (`title` is an alias) or `recipient` when you need one field. The
`opened` status includes clicked deliveries, matching the dashboard.
Every row carries `subscriberId`, `automationNodeId`, and `abTestVariantId`
alongside the delivery timestamps, so you can join rows into a recipient-level
delivery matrix without falling back to the raw event stream. Filter by `automationId`, then group the returned rows by `subscriberId`
in your application to see which contacts received which step of a sequence.
`subscriberId` is not a query parameter. Use the `recipient` query for an email
address or `automationNodeId` to list the recipients of one step directly.
This history only reaches back 14 days. For per-step send totals over the full
retained event window, read the `steps` array of
[Get Sequence Metrics](/api-reference/analytics/sequence-metrics) or call
[List Per-Email Metrics](/api-reference/analytics/email-metrics).
## Query Parameters
Case-insensitive subject or recipient search.
Case-insensitive subject/title filter. `title` is accepted as an alias.
Case-insensitive recipient email filter.
One of `pending`, `sent`, `delivered`, `opened`, `clicked`, `bounced`,
`complained`, `failed`, or `suppressed`.
One of `campaign`, `transactional`, or `sequence`.
`Permanent` or `Transient`. This also restricts status to bounced.
Filter by campaign ID.
Filter by saved transactional email ID.
Filter by sequence/automation ID.
Filter by one email step of a sequence. Take the node ID from the `steps`
array of the sequence metrics endpoint. Combined with `automationId` the two
intersect, so a node from another sequence returns no rows.
History window from 1 to 14 days.
Page number, starting at 1.
Results per page, from 1 to 100.
`recipientEmail`, `subject`, `status`, `eventAt`, `sentAt`, or `createdAt`.
`asc` or `desc`.
## Example
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/email-sends?search=welcome&status=opened&emailType=transactional&days=7" \
-H "Authorization: Bearer API_KEY"
```
```json 200 theme={null}
{
"success": true,
"retentionDays": 14,
"emailSends": [
{
"id": "send_123",
"type": "transactional",
"recipientEmail": "user@example.com",
"subscriberId": "sub_abc123",
"subject": "Welcome",
"status": "clicked",
"automationNodeId": null,
"abTestVariantId": null,
"eventAt": "2026-07-22T10:05:00.000Z",
"openedAt": "2026-07-22T10:03:00.000Z",
"clickedAt": "2026-07-22T10:05:00.000Z",
"opened": true,
"clicked": true
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
```
Pass a returned ID to
`GET /api/v1/email-sends/{emailSendId}` for the stored snapshot and complete
event timeline. A single delivery is opened or not opened: it does not have an
open rate. Use transactional email metrics for aggregate rates.
# Create Email
Source: https://docs.sequenzy.com/api-reference/emails/create
POST /api/v1/emails
Create an email template with block content.
You can create an email template with either raw HTML or structured blocks. Raw HTML is stored as a native HTML body.
## Request
Email name.
Email subject line.
Optional inbox preview text.
Raw HTML body. Provide either `html` or `blocks`, not both.
Structured email blocks. Provide either `blocks` or `html`, not both. Put
block styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/emails" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Welcome","subject":"Welcome","html":"
" }],
"createdAt": "2026-04-19T12:00:00.000Z",
"updatedAt": "2026-04-19T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "emails.create: provide either html or blocks, not both."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Update Email
Source: https://docs.sequenzy.com/api-reference/emails/update
PATCH /api/v1/emails/{emailId}
Update email metadata or replace the email body.
You can update an email's metadata and replace its body with either raw HTML or structured blocks.
## Request
Email ID.
Email name.
Email subject line.
Inbox preview text. Use `null` to clear it.
Raw HTML body. Provide either `html` or `blocks`, not both.
Structured email blocks. Provide either `blocks` or `html`, not both. Put
block styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/emails/email_123" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject":"Updated welcome","html":"
" }],
"createdAt": "2026-04-19T12:00:00.000Z",
"updatedAt": "2026-04-19T12:05:00.000Z"
}
}
```
```json 400 theme={null}
{ "success": false, "error": "No supported fields provided" }
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 404 theme={null}
{ "success": false, "error": "Email not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Update Email Blocks
Source: https://docs.sequenzy.com/api-reference/emails/update-blocks
PATCH /api/v1/emails/{emailId}/blocks
Replace an email body or mutate an existing block type.
You can replace an email body with raw HTML or structured blocks. You can also mutate an existing block between `text` and `html`.
## Request
Email ID.
Raw HTML body. Replaces all blocks with a native HTML body.
Structured email blocks. Replaces all existing blocks. Put block styling under
`styles`; top-level style keys like `backgroundColor`, `backgroundOpacity`,
`textColor`, `textAlign`, `borderColor`, `borderWidth`, and `borderRadius` are
normalized into `styles`.
Fields that are not part of a block's schema are discarded rather than
rejected, so the request still succeeds. When that happens the response
includes a `warnings` array naming each ignored field and listing the fields
that block does accept. Check `warnings` before treating a 200 as "every field
I sent took effect". See [Block styling vs block
fields](#block-styling-vs-block-fields).
Existing block ID to mutate.
New block type. Type mutation supports `text` and `html`.
Optional replacement content for the mutated block.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/emails/email_123/blocks" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"blockId":"legacy-block","type":"html"}'
```
### Custom video thumbnails
Set `thumbnailUrl` on a YouTube video block to override YouTube's still. This
endpoint replaces the blocks array, so omit `thumbnailUrl` in the replacement
block to clear the custom cover. The click destination always remains
`videoUrl`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/emails/email_123/blocks" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"blocks":[{"type":"video","videoUrl":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","thumbnailUrl":"https://cdn.example.com/product-tour.jpg","alt":"Watch the product tour"}]}'
```
## Block styling vs block fields
`styles` holds the layout and background of the block's own container - the band
the block sits in. Some blocks render an inner element whose appearance is
controlled by dedicated top-level fields instead. Setting the `styles` key
changes the band, not the inner element.
The button block is the common case:
| What you want to change | Field to set | What `styles` does instead |
| - | - | - |
| Button fill color | `buttonColor` | `styles.backgroundColor` colors the band behind it |
| Button label color | `buttonTextColor` | `styles.textColor` does not reach the label |
Leave `buttonColor` and `buttonTextColor` unset to inherit the email theme's
primary color.
Body copy color is `styles.textColor` (or the top-level `textColor` alias). An
inline `style` on the `
` or `` inside a text block's `content` is not
the field the editor and theme use.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/emails/email_123/blocks" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"blocks": [
{
"id": "cta",
"type": "button",
"text": "Buy now",
"url": "https://example.com",
"variant": "primary",
"buttonColor": "#1d4ed8",
"buttonTextColor": "#ffffff"
}
]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"email": {
"id": "email_123",
"companyId": "company_123",
"name": "Welcome",
"subject": "Welcome",
"previewText": null,
"blocks": [{ "id": "legacy-block", "type": "html", "content": "
...
" }],
"createdAt": "2026-04-19T12:00:00.000Z",
"updatedAt": "2026-04-19T12:05:00.000Z"
}
}
```
```json 200 theme={null}
{
"success": true,
"email": {
"id": "email_123",
"companyId": "company_123",
"name": "Welcome",
"subject": "Welcome",
"previewText": null,
"blocks": [
{
"id": "cta",
"type": "button",
"text": "Buy now",
"url": "https://example.com",
"variant": "primary"
}
],
"createdAt": "2026-04-19T12:00:00.000Z",
"updatedAt": "2026-04-19T12:05:00.000Z"
},
"warnings": [
"blocks[0].styles.color is not a supported field and was ignored. Button label color comes from the block-level `buttonTextColor` field. Supported styles fields: backgroundColor, backgroundOpacity, bleed, borderColor, borderRadius, borderWidth, paddingBottom, paddingLeft, paddingRight, paddingTop, textAlign, textColor."
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide blockId to mutate a block or content to replace the body."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
```json 404 theme={null}
{ "success": false, "error": "Block not found" }
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
# Get Received Event Sample
Source: https://docs.sequenzy.com/api-reference/events/sample
GET /api/v1/events/sample
Reuse the latest recorded event payload when testing a sequence
Read the latest retained event with an exact name in your selected workspace. This includes custom events and older history, across all subscribers. There is no additional recent-only cutoff. The response does not record an event, enroll a subscriber or send an email. You need `subscribers:read`; read-only workspace members can use this lookup.
Exact recorded event name. Surrounding whitespace is trimmed. No alias
resolution or published schema lookup is performed.
The trimmed event name.
Latest retained occurrence, or null when none exists. Contains `eventTime`
(UTC timestamp), `subscriberId` (the source subscriber ID) and `properties` (a
JSON object, preserving arrays, nested objects and null values). Empty
properties remain an empty object. Equally recent events have no guaranteed
tie order.
Copy `sample.properties` into the `customVariables` object for [Start Sequence Test Run](/api-reference/sequences/test-run). Review or edit the data first. The source subscriber may differ from your test recipient; copying the properties does not change that recipient. A read can be retried safely. An analytics failure returns an error, never a fabricated empty sample.
```bash theme={null}
sequenzy events sample ecommerce.checkout_started --company company_123 --json
```
With MCP, use `get_event_sample` with `eventName` and optional `companyId`, then pass the returned properties to `start_sequence_test_run`. To clear copied data, omit `customVariables` or supply `{}` on your next test. In the dashboard, clear the text field.
## Errors
* **400**: Missing or blank event name, or missing company selection for a personal key.
* **401**: Missing or invalid authentication.
* **403**: Missing `subscribers:read` or access to the requested company.
* **500**: Analytics lookup failed. Retry the read; no test run was started.
No matching event is a successful **200** response with `sample: null`, not a 404.
# Get Event Schemas
Source: https://docs.sequenzy.com/api-reference/events/schemas
GET /api/v1/events/schemas
Read the published payload of a built-in event
Read what a built-in event payload actually contains: a real example payload per provider, every property path with its type, and the merge tag that resolves it.
Use this before writing `{{event.*}}` merge tags or event property filters. An unrecognized merge tag renders as an empty string rather than an error, so a guessed property name ships silently broken.
This is static reference data describing the shape of an event, not what your account has received. For a recorded payload, use [Get Received Event Sample](/api-reference/events/sample).
## Request
Event to describe, such as `ecommerce.order_placed`. Legacy aliases like
`order.completed` resolve to their current name. Omit to list every documented
event.
Return only this provider's payload: `shopify`, `woocommerce`, `manual`,
`api`, or `stripe`. Omit to compare every provider that documents the event.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/events/schemas?eventName=ecommerce.order_placed&provider=shopify" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
The normalized event name that was described, or `null` when listing every
documented event.
One entry per event. Listing mode returns summaries only; asking for a single
`eventName` adds `providers`, `mergeTagPrefix`, and `notes`.
Whether a reference payload is published. `false` means no sample exists - it
never means the event name is invalid. Custom events are fully supported and
carry exactly the properties you send.
Providers with a reference payload for this event.
Per provider: `examplePayload` (a real sample) and `properties`.
Every property path, with `type`, the `mergeTag` that resolves it, and a
`description` wherever the sample value alone is ambiguous - a `null` sample,
an empty list, a unit that is not obvious, or a type that differs per
provider.
## Units and types worth knowing
* Fields ending in `Cents` are minor units: `8850` is \$88.50.
* `price` is a preformatted display string. Use `priceCents` for arithmetic and comparisons.
* `predictedLtv` is whole currency units, not cents, even though it sits next to `*Cents` fields.
* `churnRisk` is a percent from 0 to 95, quantized to the nearest 5 - not a 0-1 probability.
* `orderId` is a string on Shopify and the commerce API, and a number on WooCommerce. Compare it as a string.
## Responses
```json 200 theme={null}
{
"success": true,
"eventName": "ecommerce.order_placed",
"events": [
{
"eventName": "ecommerce.order_placed",
"documented": true,
"label": "Order Placed",
"category": "E-commerce",
"description": "Customer placed an order",
"documentedProviders": ["shopify", "woocommerce", "api"],
"requestedEventName": "ecommerce.order_placed",
"providers": [
{
"provider": "shopify",
"examplePayload": {
"provider": "shopify",
"orderId": "820982911946154500",
"orderNumber": 1001,
"totalPriceCents": 8850,
"currency": "USD",
"financialStatus": "paid",
"fulfillmentStatus": null,
"orderedAt": "2026-04-02T12:00:00.000Z",
"productNames": ["Protein Powder"],
"itemCount": 1,
"lineItems": [
{
"title": "Protein Powder",
"variantTitle": "Vanilla",
"quantity": 1,
"priceCents": 8850,
"price": "$88.50"
}
]
},
"properties": [
{
"path": "totalPriceCents",
"type": "number",
"description": "Total in minor units (8850 = $88.50), not a decimal amount.",
"mergeTag": "{{event.totalPriceCents}}"
},
{
"path": "fulfillmentStatus",
"type": "string | null",
"description": "Shopify fulfillment state. Null on a newly placed order - it is set on `ecommerce.order_fulfilled`.",
"mergeTag": "{{event.fulfillmentStatus}}"
},
{
"path": "lineItems",
"type": "object[]"
},
{
"path": "lineItems[].title",
"type": "string",
"mergeTag": "{{event.lineItems.0.title}}"
}
]
}
],
"mergeTagPrefix": "event.",
"notes": [
"Reference payloads are representative samples, not a contract. Providers omit fields that do not apply to a given order or product, and any extra properties you send with the event are passed through unchanged."
]
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "provider must be one of shopify, woocommerce, manual, api, stripe"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Submit Feedback
Source: https://docs.sequenzy.com/api-reference/feedback/submit
POST /api/v1/feedback
Send product feedback about Sequenzy to the Sequenzy team
Submits product feedback about Sequenzy itself. Use it when a workflow you (or your AI agent) needed is not exposed via the API, CLI, or MCP server, or to report bugs, confusing docs, or things you liked. Every submission goes directly to the Sequenzy team.
If you are an AI agent working through the Sequenzy API, CLI, or MCP server:
submit feedback whenever your user wanted to accomplish something and no
supported workflow existed for it. When something you did produced a wrong or
unexpected result, include `userIntent`, `toolCalls`, `expected`, `actual`,
and `resourceIds` so the team can reproduce it. This directly shapes what gets
built next.
## Request
The feedback itself. Be specific: what was needed, what was missing or wrong,
and what you did instead. Max 10,000 characters.
One of `missing_capability`, `bug`, `docs`, `ux`, `praise`, or `other`.
Defaults to `other`. Use `missing_capability` when a needed workflow is not
supported.
Where the feedback was submitted from: `api`, `cli`, or `mcp`. Defaults to
`api`. The official CLI and MCP server set this automatically.
Optional description of what you were trying to accomplish when you hit the
gap. Max 10,000 characters.
For bug or wrong-outcome reports: the user's request, verbatim or closely
paraphrased. Omit personal data that is not needed to reproduce the problem.
Max 5,000 characters.
For bug or wrong-outcome reports: the ordered API calls, CLI commands, or MCP
tool calls that led to the problem. Each entry is an object with `tool`
(required), `args` (a short summary of the arguments used - summarize, do not
paste raw subscriber data), and `error` (the error returned by that call, if
any). Max 25 entries.
What you expected to happen. Max 5,000 characters.
What actually happened instead. Max 5,000 characters.
IDs of the affected resources (campaign, sequence, subscriber, segment, ...)
so the team can correlate the report with server logs. Max 50 entries.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/feedback" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "There is no endpoint to bulk-delete campaigns by label.",
"category": "missing_capability",
"context": "Cleaning up 40 old promo campaigns for a client."
}'
```
Bug report with reproduction detail:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/feedback" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Updating a scheduled campaign silently dropped its schedule.",
"category": "bug",
"userIntent": "Move the promo send to 9am Tuesday",
"toolCalls": [
{ "tool": "PATCH /api/v1/campaigns/cmp_123", "args": "scheduledAt=2026-08-11T09:00" }
],
"expected": "Campaign rescheduled to Tuesday 9am",
"actual": "Campaign reverted to draft with no schedule",
"resourceIds": ["cmp_123"]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Feedback received. The Sequenzy team reviews every submission."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Expected string length greater or equal to 1"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"success": false,
"error": "No company selected"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Clear Frequency Cap
Source: https://docs.sequenzy.com/api-reference/frequency-cap/clear
DELETE /api/v1/frequency-cap
Turn the company frequency cap off
Turn your company frequency cap off. Sequences keep their `frequencyCapEnabled`
setting, but no email step is skipped until you
[set a cap](/api-reference/frequency-cap/set) again. Clearing when no cap is set
also succeeds.
Requires the `companies:manage` scope and owner or admin access.
## Request
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/frequency-cap" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Always `null`.
```json 200 theme={null}
{
"success": true,
"frequencyCap": null
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
# Get Frequency Cap
Source: https://docs.sequenzy.com/api-reference/frequency-cap/get
GET /api/v1/frequency-cap
Get the most marketing emails one contact may receive in a window
Get your company frequency cap: the most marketing emails one contact may
receive in a rolling window, such as 1 email every 16 hours.
Only sequences with `frequencyCapEnabled` respect the cap. When one of their
email steps comes up for a contact who already received the cap's number of
marketing emails in the window, the step is skipped and the contact moves on to
the next step. Campaign emails and marketing sequence emails count toward the
cap, including earlier emails from the same sequence. Campaigns always send.
Transactional emails never count, and transactional steps are never skipped.
Requires the `account:read` scope.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/frequency-cap" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
The company cap, or `null` when none is set.
Most marketing emails one contact may receive in the window, from 1 to 50.
Rolling window in hours, from 1 to 168.
```json 200 theme={null}
{
"success": true,
"frequencyCap": {
"maxEmails": 1,
"windowHours": 16
}
}
```
```json 200 (none set) theme={null}
{
"success": true,
"frequencyCap": null
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: account:read"
}
```
# Set Frequency Cap
Source: https://docs.sequenzy.com/api-reference/frequency-cap/set
PUT /api/v1/frequency-cap
Limit how many marketing emails one contact receives
Set or replace your company frequency cap. It applies from the next email step
onward in sequences with `frequencyCapEnabled`; turn that on with
[Create Sequence](/api-reference/sequences/create) or
[Update Sequence](/api-reference/sequences/update). Sequences that don't opt in
are never capped.
Emails already sent or queued in the window count, so a new cap can skip the
next step for contacts who were recently emailed.
Requires the `companies:manage` scope and owner or admin access.
## Request
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/frequency-cap" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxEmails": 1, "windowHours": 16 }'
```
## Body
Most marketing emails one contact may receive in the window, from 1 to 50.
Rolling window in hours, from 1 to 168.
## Response fields
The saved cap, with `maxEmails` and `windowHours`.
```json 200 theme={null}
{
"success": true,
"frequencyCap": {
"maxEmails": 1,
"windowHours": 16
}
}
```
```json 400 theme={null}
{
"error": "maxEmails must be an integer from 1 to 50"
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
A `400` is returned when either field is missing, not a whole number or out of
range. Nothing is saved, so fix the value and retry.
# Generate Email
Source: https://docs.sequenzy.com/api-reference/generate/email
POST /api/v1/generate/email
Generate a draft email from a natural-language prompt
Generates a draft email from scratch as structured editor-compatible blocks.
By default, Sequenzy wraps the generated content with your company's email
branding: branded presets include a logo and full footer, while minimal presets
include a simple footer. Review the returned blocks before creating or sending
a campaign.
## Request
What you want the email to say or accomplish.
Optional style guidance. Pass `designed` or `plain` to force the designed or
plain-text email style; other values, such as `minimal`, `branded`, or
`promotional`, are freeform prompt guidance. Defaults to your company's email
style preference (designed unless you chose plain text).
Optional tone guidance, such as `professional`, `casual`, or `friendly`.
Whether to wrap the generated content with your company logo and footer. Set
this to `false` to return only the AI-generated content blocks.
Email type: `marketing` or `transactional`. Transactional emails include a
footer without an unsubscribe link.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/generate/email" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Announce our new analytics dashboard to trial users",
"style": "branded",
"tone": "friendly",
"emailType": "marketing"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"subject": "Meet your new analytics dashboard",
"previewText": "A clearer way to track what matters",
"blocks": [
{
"id": "block-logo",
"type": "logo",
"alt": "Acme Logo",
"align": "center",
"width": 64
},
{
"id": "block-heading",
"type": "heading",
"level": 1,
"content": "Meet your new analytics dashboard"
},
{
"id": "block-footer",
"type": "footer",
"variant": "full",
"companyName": "Acme",
"address": "123 Main St"
}
],
"message": "Generated a draft email."
}
```
```json 400 theme={null}
{
"type": "validation",
"on": "body",
"property": "/prompt",
"message": "Expected string"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 500 theme={null}
{
"error": "Failed to generate email"
}
```
# Generate Sequence (Deprecated)
Source: https://docs.sequenzy.com/api-reference/generate/sequence
POST /api/v1/generate/sequence
Deprecated compatibility alias that creates a disabled AI-generated sequence draft
This endpoint is deprecated. Use `POST /api/v1/sequences` with `name`,
`trigger`, and `goal` for new integrations.
Creates and persists a disabled sequence draft using a `contact_added` trigger.
The endpoint remains as a compatibility alias; it returns `Deprecation: true`
and a successor `Link` header. It never enables the sequence.
## Request
The sequence goal or desired subscriber journey.
Optional sequence name. Defaults to a normalized version of `goal`.
Optional list ID that scopes the `contact_added` trigger.
Number of emails to generate. Defaults to `5`. Maximum is `10`.
Duration used to space suggested delays. Defaults to `14`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/generate/sequence" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Workspace onboarding",
"goal": "Onboard a new workspace admin",
"emailCount": 3,
"durationDays": 7
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"deprecated": true,
"deprecationMessage": "POST /api/v1/generate/sequence is deprecated. Use POST /api/v1/sequences with goal-based creation.",
"sequence": {
"id": "seq_123",
"name": "Workspace onboarding",
"status": "draft",
"trigger": "trigger_list",
"emailCount": 3,
"enrichmentStatus": "processing"
},
"message": "Sequence created. AI is generating email content based on your brand. This typically takes 30-60 seconds."
}
```
```json 400 theme={null}
{
"error": "emailCount must be a positive integer"
}
```
```json 400 theme={null}
{
"error": "emailCount must be no more than 10"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 500 theme={null}
{
"error": "Failed to create sequence"
}
```
# Generate SMS Messages
Source: https://docs.sequenzy.com/api-reference/generate/sms
POST /api/v1/generate/sms
Generate SMS message variants from a prompt
Generates draft SMS marketing messages with per-message encoding and segment counts. Use a generated message as the `text` of an SMS sequence step.
Messages never include opt-out footers or a brand-name prefix - Sequenzy adds both automatically at send time.
## Request
Description of the SMS to generate, e.g. "cart reminder with a free-shipping
hook".
Number of variants to generate (1-10). Defaults to `3`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/generate/sms" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Cart reminder with a free-shipping hook",
"count": 3
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"prompt": "Cart reminder with a free-shipping hook",
"messages": [
{
"text": "Hey {{FIRST_NAME}}, your cart is waiting! Complete your order today and shipping is on us.",
"encoding": "gsm7",
"segments": 1,
"characterCount": 94
}
]
}
```
```json 400 theme={null}
{
"error": "count must be an integer between 1 and 10"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 500 theme={null}
{
"error": "Failed to generate SMS messages"
}
```
# Generate Subject Lines
Source: https://docs.sequenzy.com/api-reference/generate/subjects
POST /api/v1/generate/subjects
Generate subject line variants for a topic
Generates draft subject line variants for a campaign or sequence email.
## Request
Topic, campaign idea, or context for the subject lines.
Number of variants to generate. Defaults to `5`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/generate/subjects" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"topic": "April product launch",
"count": 5
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"topic": "April product launch",
"subjects": [
"Meet what we shipped in April",
"Your April product update is here"
]
}
```
```json 400 theme={null}
{
"error": "count must be a positive integer"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 500 theme={null}
{
"error": "Failed to generate subject lines"
}
```
# List Integration Activity
Source: https://docs.sequenzy.com/api-reference/integrations/activity
GET /api/v1/integrations/activity
Read recent integration webhook and sync activity
Read recent integration webhook and sync activity, newest first. This is the log to check when an integration reports connected but contacts or events are not appearing.
Activity is retained for 24 hours. Payloads are sanitized when written, so no
credentials, tokens, or signatures appear here.
Requires an API key with both the `account:read` and `subscribers:read` scopes.
## Request
Only show activity for this integration.
Only show activity for this provider, such as `stripe`.
Filter by `received`, `queued`, `processed`, `skipped`, or `failed`. Use
`failed` to triage a broken integration.
Rows to return, from 1 to 100. Out-of-range values are clamped.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations/activity?status=failed" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Where the event got to. A row stuck on `queued` or `received` for more than 15
minutes indicates background workers are not draining the queue.
The provider's own event type, such as `invoice.payment_failed`.
The contact the event was matched to, or `null` when no contact could be
resolved - the most common reason for an event to be skipped.
Retention window in hours. Nothing older is queryable.
## Responses
```json 200 theme={null}
{
"success": true,
"windowHours": 24,
"activity": [
{
"id": "ial_abc123",
"provider": "stripe",
"integrationId": "int_abc123",
"source": "webhook",
"action": "process_webhook",
"status": "failed",
"eventType": "invoice.payment_failed",
"providerEventId": "evt_abc123",
"jobId": "job_abc123",
"subscriberId": "sub_abc123",
"email": "buyer@example.com",
"externalId": "cus_abc123",
"message": null,
"error": "Subscriber not found",
"requestPayload": { "eventType": "invoice.payment_failed" },
"responsePayload": { "success": false },
"receivedAt": "2026-08-01T09:13:59Z",
"processedAt": "2026-08-01T09:14:00Z",
"expiresAt": "2026-08-02T09:14:00Z",
"createdAt": "2026-08-01T09:14:00Z"
}
],
"note": "Integration activity is retained for 24 hours."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: subscribers:read"
}
```
# Get Attio List Mapping
Source: https://docs.sequenzy.com/api-reference/integrations/attio-get
GET /api/v1/integrations/{id}/attio
Read a connected Attio integration's list map and live Attio people-lists
Read a connected Attio integration's saved Sequenzy-to-Attio list map, this company's Sequenzy lists, and live Attio people-lists.
Attio is a no-op until at least one Sequenzy list is mapped. Connect can succeed with `mappedListCount` 0; this endpoint is how you see what to map without reconnecting or asking for the access token again.
People-lists are read from Attio on every call using the stored token, because a list can be deleted in Attio without Sequenzy hearing about it.
Attio only. Other providers return a `400`.
Requires an API key with the `account:read` and `lists:read` scopes.
## Request
Attio integration ID, from [List
Integrations](/api-reference/integrations/list).
This endpoint takes no request body.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations/int_abc123/attio" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Saved Sequenzy list id → Attio list UUID or api slug. Empty when nothing is
mapped.
Number of saved mappings whose Sequenzy source and Attio target lists both
still exist. Zero means list joins are not synced. Stale entries remain in
`listMap` for recovery but are not included in this count, and `message`
identifies them as stale.
Whether a company is upserted from the person's non-free-mail email domain.
This company's Sequenzy lists (`id`, `name`).
Attio people-lists the stored token can write (`id`, `apiSlug`, `name`), read
live from Attio.
Save mappings with [Update Attio List Mapping](/api-reference/integrations/attio-update).
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "attio",
"listMap": {},
"mappedListCount": 0,
"syncCompanyFromDomain": true,
"sequenzyLists": [
{ "id": "list_abc", "name": "Field Guide" },
{ "id": "list_def", "name": "Benchmark" }
],
"attioLists": [
{ "id": "list_test", "apiSlug": "field-guide", "name": "Field Guide" },
{ "id": "list_benchmark", "apiSlug": "benchmark", "name": "Benchmark" }
],
"message": "No active Sequenzy lists are mapped, so list joins are not synced to Attio. Map them with PATCH /v1/integrations/{id}/attio (CLI: `sequenzy integrations set-attio-lists `, MCP: update_attio_settings)."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Stripe is not an Attio integration. Only Attio has Sequenzy-to-Attio list mappings."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
# Update Attio List Mapping
Source: https://docs.sequenzy.com/api-reference/integrations/attio-update
PATCH /api/v1/integrations/{id}/attio
Map Sequenzy lists to Attio lists without reconnecting
Save Sequenzy-to-Attio list mappings and/or company-matching on an already-connected Attio integration, using the stored access token.
This does **not** require the Attio access token again. Do not reconnect just to change mappings.
`listMap` is a full replacement when provided. Pass the complete desired map, `{}` to unmap every list, or omit `listMap` to leave mappings unchanged. Unmapped Sequenzy lists are not synced. Unmapping does not remove anyone from Attio: the sync is add-only and does not backfill people already on a list.
Provide at least one of `listMap` or `syncCompanyFromDomain`. Idempotent: asking for the current state succeeds with `changed: false`.
Attio only. Other providers return a `400`.
Requires an API key with the `integrations:manage` scope. Agent-safe keys do not carry it, by design: an agent can read the current mapping from [Get Attio List Mapping](/api-reference/integrations/attio-get) and report the dashboard path (Settings → Integrations → Attio → Map lists), but applying it needs a key with integration management rights.
## Request
Attio integration ID, from [List
Integrations](/api-reference/integrations/list).
Complete map of Sequenzy list id to Attio people-list UUID or api slug.
Replaces the saved map. Pass `{}` to clear every mapping. Omit to leave
mappings unchanged. Unknown Sequenzy ids and unknown or non-people Attio
targets are rejected.
When true, upsert a company from the person's non-free-mail email domain. Omit
to leave the current setting unchanged.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/integrations/int_abc123/attio" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"listMap": {
"list_abc": "field-guide",
"list_def": "benchmark"
}
}'
```
## Response fields
Same shape as [Get Attio List Mapping](/api-reference/integrations/attio-get), plus:
False when the integration was already in the requested state.
Which settings actually moved: `listMap` and/or `syncCompanyFromDomain`.
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "attio",
"listMap": {
"list_abc": "field-guide",
"list_def": "benchmark"
},
"mappedListCount": 2,
"syncCompanyFromDomain": true,
"sequenzyLists": [
{ "id": "list_abc", "name": "Field Guide" },
{ "id": "list_def", "name": "Benchmark" }
],
"attioLists": [
{ "id": "list_test", "apiSlug": "field-guide", "name": "Field Guide" },
{ "id": "list_benchmark", "apiSlug": "benchmark", "name": "Benchmark" }
],
"changed": true,
"changedFields": ["listMap"],
"message": "2 active Sequenzy list(s) mapped to Attio. Unmapped lists are not synced."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unknown or non-people Attio list id(s): not-a-list"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
# List Integration Capabilities
Source: https://docs.sequenzy.com/api-reference/integrations/catalog
GET /api/v1/integrations/catalog
Describe what each integration provider syncs, emits, and supports
Describe what Sequenzy's integration providers do, whether or not they are connected. Use this to decide what to connect, to compare providers, or to check that an event a sequence needs actually exists for a given provider.
Coverage differs sharply between providers. Only Shopify has storefront browse tracking, and only Stripe and Chargebee cover the full trial lifecycle - so check here rather than assuming a provider emits an event.
## Request
Return only this provider, such as `stripe` or `shopify`. Returns 404 for an
unknown provider.
Filter by category: `payments`, `ecommerce`, `auth`, `analytics`, `ads`,
`affiliate`, `cms`, `crm`, or `developer`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations/catalog?provider=stripe" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`available`, `beta`, or `coming_soon`. A `coming_soon` provider appears in the
dashboard picker but has no webhook handler yet, so it emits nothing.
How the provider is connected: `oauth`, `api_key`, `app_install`, `plugin`, or
`webhook`. OAuth and app installs always need a person in the dashboard.
What the integration keeps in sync, such as `customers`, `orders`, or
`products`. Empty when the provider only pushes events as they happen.
Every event the provider can produce. Each entry has `event` (the name a
sequence triggers on) and `when` (the real-world moment that produces it). An
empty array means the provider never triggers automations.
Subscriber attributes the provider writes, each with a `key` and
`description`. Use these in segment filters.
Actions supported on a connected integration. Varies by provider: `connect`,
`enable_sync`, `disable_sync`, `sync_now`, `sync_products`,
`set_list_targeting`, `activate_pixel`, `update_attio_settings`.
Caveats worth knowing before building on the provider, such as which events it
does not emit.
## Responses
```json 200 theme={null}
{
"success": true,
"providers": [
{
"provider": "stripe",
"name": "Stripe",
"category": "payments",
"availability": "available",
"summary": "Syncs Stripe customers, subscriptions, and products, and turns billing webhooks into subscriber events, tags, and revenue attributes.",
"connectMethod": "oauth",
"syncs": ["customers", "subscriptions", "products", "revenue"],
"emits": [
{
"event": "saas.purchase",
"when": "a customer completes a purchase or starts a paid subscription"
},
{
"event": "saas.payment_failed",
"when": "a payment attempt fails (card declined, expired, insufficient funds)"
}
],
"writesAttributes": [
{
"key": "stripeCustomerId",
"description": "The customer's stripe customer ID, copied on every sync."
},
{
"key": "mrr",
"description": "Monthly recurring revenue for the customer, as a number."
}
],
"actions": ["enable_sync", "disable_sync", "sync_now", "sync_products"],
"notes": [
"The only payment provider with full trial lifecycle coverage plus upgrade/downgrade detection."
]
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration provider not found"
}
```
# Connect Integration
Source: https://docs.sequenzy.com/api-reference/integrations/connect
POST /api/v1/integrations/connect
Connect an API-key or webhook-secret integration
Connect an integration whose setup only needs an API key and/or a webhook signing secret: Polar, Paddle, Dodo Payments, Lemon Squeezy, Whop, Creem, Chargebee, Clerk, PostHog, Segment, Affonso, or Attio.
Credentials are validated against the provider where possible, stored encrypted, and never returned by any endpoint. Payment providers queue their initial revenue backfill automatically; Affonso queues its affiliate backfill; PostHog and Segment can optionally import their event history. Attio is outbound-only and returns an empty `webhookUrl`.
For Lemon Squeezy, omit `webhookSecret` to create the signed webhook automatically, or supply it for manual setup. For other inbound providers, configure the returned `webhookUrl` with the same secret you passed here. Calling this for an already-connected provider replaces its stored credentials; Lemon Squeezy managed reconnections rotate the secret.
OAuth and app-install providers (Stripe, Shopify, Supabase, GitHub, WooCommerce, Meta) cannot be connected here: their flows need a browser, so they return a 400 pointing at the dashboard. Use [Integration Catalog](/api-reference/integrations/catalog) to discover each connectable provider's exact fields via `connectFields`.
Requires an API key with the `integrations:manage` scope.
## Request
One of `polar`, `paddle`, `dodo`, `lemon_squeezy`, `whop`, `creem`,
`chargebee`, `clerk`, `posthog`, `segment`, `affonso`, `attio`.
Signing secret of the webhook you create at the provider, pointed at the
returned `webhookUrl`. Optional for Lemon Squeezy managed provisioning and
Attio, which is outbound-only; required for other providers. For Chargebee,
pass the webhook's basic-auth credentials as `username:password`. Lemon
Squeezy manual secrets must be 16-40 characters. For Segment, the secret is
your own choice and must be between 16 and 153 UTF-8 bytes.
Provider API key. Required for polar, paddle, dodo, lemon\_squeezy, whop,
creem, chargebee, affonso, and attio. Attio uses the workspace access token
from Settings → Developers.
Provider account ID. Required for Paddle (seller ID), Dodo (business ID),
Lemon Squeezy (numeric store ID), Whop (company ID, `biz_...`), Creem (store
ID), and Chargebee (site name). Polar resolves it from the API key; Clerk,
PostHog, Segment, and Affonso derive it.
PostHog and Segment: `{ syncAllEvents?: boolean, eventAllowlist?: string[] }`.
PostHog defaults to syncing every non-internal event. New Segment connections
sync track and identify calls but skip automatic `page` and `screen` calls
unless those names are explicitly allowlisted. Attio:
`{ listMap?: Record, syncCompanyFromDomain?: boolean }`,
where `listMap` is Sequenzy list id → Attio list UUID or api slug
(`syncCompanyFromDomain` defaults to `true`). Mapping can also be set after
connect with [Update Attio List Mapping](/api-reference/integrations/attio-update)
without the access token.
PostHog and Segment only: imports event history after connecting. PostHog:
`{ region: "us" | "eu", projectId: string, personalApiKey: string }` - the
personal API key needs query read access and the whole project archive is
imported. Segment: `{ region: "us" | "eu", spaceId: string, profileApiToken:
string }` - Segment has no bulk event export and its Profile API serves at
most the last 14 days of a profile's events, so the import walks your
existing contacts and reads each one's recent Unify history by email;
contacts without a Segment profile are skipped.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/integrations/connect" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "paddle",
"apiKey": "pdl_live_apikey_...",
"webhookSecret": "pdl_ntfset_...",
"providerAccountId": "12345"
}'
```
## Response fields
The connected integration, in the same shape as [List
Integrations](/api-reference/integrations/list). Credentials are never
included.
URL to configure in the provider's webhook settings with the same secret.
Empty for Attio, which is outbound-only.
Payment providers only: whether the initial revenue backfill was queued.
Lemon Squeezy only: `managed` when Sequenzy installed the webhook, or `manual`
when the caller supplied its signing secret.
Lemon Squeezy only: whether the API key and store are in test mode.
Affonso only: whether the affiliate backfill was queued.
PostHog and Segment only: `{(requested, queued, error)}` for the optional
history import. The webhook connection succeeds even when queueing the import
failed.
## Responses
```json 200 theme={null}
{
"success": true,
"integration": {
"id": "int_abc123",
"provider": "paddle",
"name": "Paddle",
"providerAccountId": "12345",
"isActive": true,
"syncEnabled": true,
"syncStatus": null,
"connectedAt": "2026-08-02T10:00:00.000Z"
},
"webhookUrl": "https://api.sequenzy.com/api/webhooks/paddle/comp_123",
"revenueSyncQueued": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "Stripe cannot be connected through the API: its connection runs an OAuth, app-install, or plugin flow that needs a human in the dashboard. Connect it from Settings -> Integrations."
}
```
```json 400 Invalid provider credential theme={null}
{
"success": false,
"error": "Invalid Polar API key. Please check and try again."
}
```
```json 400 Missing provider permissions theme={null}
{
"success": false,
"error": "The Whop API key is missing one or more required read permissions."
}
```
```json 409 theme={null}
{
"success": false,
"error": "This Paddle account is already connected to \"Other Workspace\""
}
```
# Disconnect Integration
Source: https://docs.sequenzy.com/api-reference/integrations/disconnect
POST /api/v1/integrations/{id}/disconnect
Stop Lemon Squeezy ingestion and remove its managed webhook
Disconnect your Lemon Squeezy integration using its integration ID. No request
body is needed. Your API key needs `integrations:manage`; personal keys also need
owner or admin access to the selected company.
Local ingestion stops before provider cleanup. Existing subscribers and history
remain. Sequenzy removes managed webhooks only; manual webhooks stay under your
control. In-flight syncs and queued webhooks cannot reactivate the connection.
A successful response includes `success`, `integrationId`, `provider`,
`cleanupWarning`, and `message`. `cleanupWarning` is `null` after successful
cleanup or when there is no managed webhook. A warning still means you are
disconnected locally. Repeat the same request to retry cleanup, including when
the integration is inactive. Repeated successful cleanup is safe.
Other providers return `400` and require the dashboard. Missing or invalid keys
return `401`, insufficient scopes or roles return `403`, and an ID outside your
selected company returns `404`. A store change during disconnection returns
`409`; retry using the current integration. A local failure returns `500`; retry
the request. Provider cleanup failure is a `200` response with a warning.
Use [Connect Integration](/api-reference/integrations/connect) to reconnect with
valid credentials and resume ingestion.
# Get Integration
Source: https://docs.sequenzy.com/api-reference/integrations/get
GET /api/v1/integrations/{id}
Inspect one integration: what it does, its events, and its wiring
Inspect a connected integration in depth: what the provider syncs, every event it can emit, the tags each event applies through your sync rules, which sequences trigger on those events, recent activity, and prioritized recommendations.
The most useful field is `unusedEvents` - the events this integration produces that no sequence acts on. That is the difference between "the integration is broken" and "the integration works but nothing is built on it yet".
Read-only. Credentials, access tokens, and webhook secrets are never returned.
Requires an API key with the `account:read`, `subscribers:read`,
`sequences:read`, and `lists:read` scopes. The last one covers the `ingestion`
block, which names the lists this integration's contacts join.
## Request
Integration ID, from [List Integrations](/api-reference/integrations/list).
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations/int_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
The connection itself, with the same fields as the list endpoint plus `name`
(display name) and `category`.
The provider's catalog entry - what it syncs, what it emits, what it writes.
See [List Integration Capabilities](/api-reference/integrations/catalog).
`null` for a provider with no catalog entry.
Each emitted event crossed with your account's configuration: aggregate
`addsTags`/`removesTags`, every matching entry in `rules` with its own
conditions, listening sequences, and account-wide observation fields.
Events the provider emits that no sequence triggers on. These are the concrete
automation gaps for this integration.
Provider event names the account has never received from any source. Use the
integration-specific 24-hour activity log to diagnose this connection.
Webhook and sync activity over the last 24 hours: totals by status, a
`stalled` count for events queued more than 15 minutes without completing,
`lastActivityAt`, and up to five `recentFailures`.
Shopify only: live storefront tracking pixel state, read from the store on
every call. `null` for providers that have no pixel. `healthy` is the field to
branch on, while `error` distinguishes a confirmed dark pixel from a state
Shopify could not confirm. `dependentEvents` lists the events that rely on the
pixel. See [Get Storefront Pixel](/api-reference/integrations/pixel-get).
Prioritized problems and suggestions. Each has a `code`, a `severity` of
`error`, `warning`, or `info`, a `message`, and a concrete `action`. A dark
storefront pixel is reported here as `pixel_not_installed` (or
`pixel_configuration_stale`), and is raised to `error` severity when a
sequence is already waiting on an event the pixel is not delivering. A
connected Attio integration with no list mappings is `attio_lists_unmapped`;
map them with [Update Attio List
Mapping](/api-reference/integrations/attio-update).
Actions callable right now given the integration's current state. A
disconnected integration returns an empty array, except that a Lemon Squeezy
integration with pending managed webhook cleanup returns `disconnect` so you
can retry cleanup.
## Responses
```json 200 theme={null}
{
"success": true,
"integration": {
"id": "int_abc123",
"provider": "stripe",
"name": "Stripe",
"category": "payments",
"providerAccountId": "acct_1ABC",
"isActive": true,
"syncEnabled": true,
"syncStatus": "idle",
"lastSyncAt": "2026-08-01T10:30:00Z",
"lastSyncError": null,
"totalCustomersSynced": 4821,
"totalEventsSynced": 19422,
"connectedAt": "2026-01-14T08:12:00Z",
"disconnectedAt": null,
"details": {},
"lastSyncSkipped": null
},
"capabilities": {
"provider": "stripe",
"name": "Stripe",
"category": "payments",
"availability": "available",
"connectMethod": "oauth",
"syncs": ["customers", "subscriptions", "products", "revenue"],
"emits": [],
"writesAttributes": [],
"actions": ["enable_sync", "disable_sync", "sync_now", "sync_products"]
},
"events": [
{
"event": "saas.payment_failed",
"when": "a payment attempt fails (card declined, expired, insufficient funds)",
"addsTags": ["past-due"],
"removesTags": [],
"rules": [{ "addsTags": ["past-due"], "removesTags": [], "conditions": { "requiresTags": ["customer"] } }],
"observedByAccount": false,
"accountLastSeenAt": null,
"ruleSource": "default",
"listeners": []
},
{
"event": "saas.purchase",
"when": "a customer completes a purchase or starts a paid subscription",
"addsTags": ["customer"],
"removesTags": ["lead", "past-due"],
"rules": [{ "addsTags": ["customer"], "removesTags": ["lead", "past-due"], "conditions": null }],
"observedByAccount": true,
"accountLastSeenAt": "2026-08-01T10:25:00Z",
"ruleSource": "default",
"listeners": [
{
"sequenceId": "seq_123",
"name": "New customer onboarding",
"effectiveStatus": "live",
"acceptsNewEnrollments": true,
"effectiveStatusSummary": "Live. New subscribers enroll and existing recipients keep receiving steps."
}
]
}
],
"unusedEvents": ["saas.payment_failed"],
"activity": {
"windowHours": 24,
"total": 128,
"processed": 127,
"failed": 1,
"skipped": 0,
"stalled": 0,
"lastActivityAt": "2026-08-01T11:02:00Z",
"recentFailures": [
{
"action": "process_webhook",
"eventType": "invoice.payment_failed",
"email": "buyer@example.com",
"error": "Subscriber not found",
"createdAt": "2026-08-01T09:14:00Z"
}
]
},
"recommendations": [
{
"code": "events_without_sequences",
"severity": "info",
"message": "Stripe can emit 1 event(s) that no sequence triggers on: saas.payment_failed.",
"action": "Create a sequence with an event trigger for the ones worth acting on, for example a dunning sequence on saas.payment_failed."
}
],
"availableActions": ["disable_sync", "sync_now", "sync_products"]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: sequences:read"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
# List Integrations
Source: https://docs.sequenzy.com/api-reference/integrations/list
GET /api/v1/integrations
List connected integrations and their sync health
List the external systems connected to a company, with connection state, sync health, and the last sync error.
This endpoint is read-only and never returns credentials. Access tokens,
refresh tokens, webhook secrets, and provider API keys are excluded. The
`details` object carries only an allowlist of non-secret descriptive fields.
## Request
Include disconnected integrations. By default only active integrations are
returned.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Provider identifier, such as `stripe`, `shopify`, `supabase`, or `clerk`.
Display name for the provider, such as `Shopify App Billing` for
`shopify_app`. Falls back to the raw provider id, so it is always safe to
render.
Provider category: `payments`, `ecommerce`, `auth`, `analytics`, `ads`,
`affiliate`, `cms`, or `developer`. `null` for a provider with no catalog
entry.
Provider-side account identifier, such as a Shopify shop domain or a Stripe
`acct_` ID.
Last known sync state, such as `idle`, `syncing`, or `error`.
Error message from the most recent failed sync, or `null`.
Allowlisted non-secret metadata, such as `shopName`, `primaryDomain`, or
`scopes`. Attio includes `listMap`, `mappedListCount`, and
`syncCompanyFromDomain` so non-dashboard clients can inspect the saved
configuration.
Records the last sync could not import normally, or `null` when the sync was
clean. `suppressed` counts profiles that were imported but cannot receive
email, so their order history still counts while sends stay blocked. `skipped`
counts records that were not imported at all. `records` holds a sample of up
to 50, and `truncated` is `true` when more were affected than the sample
shows.
## Responses
```json 200 theme={null}
{
"success": true,
"integrations": [
{
"id": "int_abc123",
"provider": "shopify",
"providerAccountId": "acme.myshopify.com",
"isActive": true,
"syncEnabled": true,
"syncStatus": "idle",
"lastSyncAt": "2026-05-01T10:30:00Z",
"lastSyncError": null,
"totalCustomersSynced": 4821,
"totalEventsSynced": 19422,
"connectedAt": "2026-01-14T08:12:00Z",
"disconnectedAt": null,
"details": { "shopName": "Acme", "primaryDomain": "acme.com" },
"lastSyncSkipped": {
"total": 1,
"suppressed": 1,
"skipped": 0,
"truncated": false,
"records": [
{
"email": "buyer@closed-domain.example",
"outcome": "suppressed",
"reasonCode": "invalid_email_domain",
"reason": "Invalid domain for \"buyer@closed-domain.example\": Domain cannot receive email (no MX records)"
}
]
}
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
# Activate Storefront Pixel
Source: https://docs.sequenzy.com/api-reference/integrations/pixel-activate
POST /api/v1/integrations/{id}/pixel
Install the Shopify storefront tracking pixel, or repoint an existing one
Install the storefront tracking pixel on a connected Shopify store, or repoint an existing pixel at this account.
This is the step that turns on product views, cart activity, and browse abandonment - the data every browse-abandonment and cart-recovery sequence depends on. Run it before building those sequences: events start flowing on the next storefront visit, and nothing is backfilled for the period the pixel was off.
Idempotent. A pixel that is already installed and current returns `changed: false` without writing to the store.
Shopify only. Other providers return a `400`.
Requires an API key with the `integrations:manage` scope.
## Request
Shopify integration ID, from [List
Integrations](/api-reference/integrations/list).
This endpoint takes no request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/integrations/int_abc123/pixel" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`false` when the pixel was already installed and reporting to this account.
`true` when a new pixel was installed on the store.
`true` when an existing pixel was repointed at this account.
Pixel state after activation. Same shape as [Get Storefront
Pixel](/api-reference/integrations/pixel-get).
Event names this pixel enables.
## Permissions
If the store granted the app an older permission set, Shopify refuses the install and this endpoint returns a `400` naming the reconnect step. Reconnect Shopify from **Settings → Integrations** in the dashboard to grant storefront tracking permissions, then retry.
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "shopify",
"shopDomain": "example.myshopify.com",
"pixel": {
"installed": true,
"id": "gid://shopify/WebPixel/4124934472",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/shopify/comp_123/customer-events",
"endpointCurrent": true,
"configurationCurrent": true,
"healthy": true,
"error": null
},
"changed": true,
"created": true,
"dependentEvents": [
"ecommerce.product_viewed",
"ecommerce.collection_viewed",
"ecommerce.search_submitted",
"ecommerce.product_added_to_cart",
"ecommerce.cart_started",
"ecommerce.cart_cleared",
"ecommerce.browse_abandoned",
"ecommerce.cart_abandoned"
],
"message": "Storefront pixel installed. On-site events start arriving on the next storefront visit; nothing is backfilled for visits that happened while it was off."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Shopify refused to install the pixel because the app is missing the required storefront tracking permissions (ACCESS_DENIED: Access denied for webPixelCreate). Reinstall or reconnect Shopify from Settings -> Integrations in the dashboard to grant them, then retry."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
# Get Storefront Pixel
Source: https://docs.sequenzy.com/api-reference/integrations/pixel-get
GET /api/v1/integrations/{id}/pixel
Check whether a Shopify store's tracking pixel is installed and reporting
Read the live state of a Shopify store's storefront tracking pixel.
The pixel is what produces on-site events: product views, collection views, storefront search, cart activity, and browse abandonment. Without it those events never arrive, and any sequence triggered by one silently never fires.
Nothing about the pixel is stored on the Sequenzy side. A merchant can remove it from the Shopify admin at any time, so every call reads the store directly.
Shopify only. Other providers return a `400`.
Requires an API key with the `account:read` scope.
## Request
Shopify integration ID, from [List
Integrations](/api-reference/integrations/list).
This endpoint takes no request body.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/integrations/int_abc123/pixel" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Whether a pixel for this app exists on the store.
Shopify's pixel ID, or `null` when none is installed.
Callback URL the installed pixel posts events to.
Whether the installed pixel posts to this account's canonical endpoint or the
supported Shopify compatibility endpoint.
Whether the endpoint and signed company, shop, and integration settings all
match this connection. This catches a pixel whose URL looks current but whose
events ingress would reject.
Installed, current, and confirmed by Shopify. This is the single field to
check before relying on any on-site event.
Set when Shopify could not be reached or refused the query. `null` on a clean
read. An error is not the same answer as "not installed" - it means the state
is unknown.
Event names that depend on this pixel. They are confirmed unable to arrive
only when `pixel.error` is `null` and `pixel.healthy` is `false`; an error
means Shopify could not confirm their state.
Fix a confirmed missing or stale pixel with [Activate Storefront Pixel](/api-reference/integrations/pixel-activate). Retry an unknown read before changing the store.
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "shopify",
"shopDomain": "example.myshopify.com",
"pixel": {
"installed": true,
"id": "gid://shopify/WebPixel/4124934472",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/shopify/comp_123/customer-events",
"endpointCurrent": true,
"configurationCurrent": true,
"healthy": true,
"error": null
},
"dependentEvents": [
"ecommerce.product_viewed",
"ecommerce.collection_viewed",
"ecommerce.search_submitted",
"ecommerce.product_added_to_cart",
"ecommerce.cart_started",
"ecommerce.cart_cleared",
"ecommerce.browse_abandoned",
"ecommerce.cart_abandoned"
],
"message": "The storefront pixel is installed and reporting to this account."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Stripe has no storefront tracking pixel. Only Shopify does."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
# Sync Integration
Source: https://docs.sequenzy.com/api-reference/integrations/sync
POST /api/v1/integrations/{id}/sync
Queue a manual re-sync for a connected integration
Queue a manual re-sync: customers and revenue for a payment provider, the user backfill for Supabase, or the event-history import for PostHog or Segment.
Supported for Stripe, Polar, Paddle, Dodo, Lemon Squeezy, Creem, Chargebee,
Whop, Supabase, PostHog, and Segment. Other providers re-sync from the dashboard.
For PostHog this re-runs the event-history import against the personal API key, region, and project stored at connect time, and it is the supported way to retry a failed import. The import restarts from the beginning; already-imported events carry the same IDs the live webhook uses, so a re-run cannot duplicate them. Historical events never trigger automations. You get a `409` while an import is queued or syncing; terminal BullMQ failures release that state for retry.
For Segment this re-runs the event-history import against the Unify space ID,
region, and Profile API token stored at connect time. It walks existing contacts'
profiles and covers the most recent 14 days served by the Profile API. This is
the supported retry for a failed import; it restarts from the beginning and
deduplicates events already imported or received live. A missing stored space or
token returns `400`, and an import already queued or syncing returns `409`.
For Supabase this reads the project, schema, and table already configured for the integration - it cannot be pointed at a different table, and it returns `400` when no table has been configured yet. Run it after connecting Supabase: the database trigger only sends rows that change after it is installed, so users who existed before that have no attributes in Sequenzy until a backfill runs.
The call returns immediately. Poll [Get Integration](/api-reference/integrations/get) to watch `syncStatus` and `lastSyncAt`, and [List Integration Activity](/api-reference/integrations/activity) for per-row outcomes.
Requires an API key with the `integrations:manage` scope.
## Request
Integration ID, from [List Integrations](/api-reference/integrations/list).
This endpoint takes no request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/integrations/int_abc123/sync" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Background job ID for the queued sync, or `null` when no job ID was returned.
Sync status after queueing, always `queued` on success.
Supabase only: the `projectRef`, `schema`, and `table` the backfill reads.
Absent for providers whose sync has no configurable source.
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "stripe",
"jobId": "1042",
"syncStatus": "queued",
"message": "Sync queued for Stripe. Poll the integration to watch syncStatus and lastSyncAt."
}
```
```json 200 theme={null}
{
"success": true,
"integrationId": "int_segment123",
"provider": "segment",
"jobId": "4421",
"syncStatus": "queued",
"message": "Segment event history import queued. It walks your existing contacts' Unify profiles from the beginning and re-imported events dedupe, so nothing is duplicated. Poll the integration to watch syncStatus and lastSyncAt."
}
```
```json 200 theme={null}
{
"success": true,
"integrationId": "int_def456",
"provider": "supabase",
"jobId": "2087",
"syncStatus": "queued",
"syncTarget": {
"projectRef": "abcdefgh",
"schema": "public",
"table": "profiles"
},
"message": "Supabase user sync queued for public.profiles. Poll the integration to watch syncStatus and lastSyncAt."
}
```
```json 200 theme={null}
{
"success": true,
"integrationId": "int_ghi789",
"provider": "posthog",
"jobId": "3312",
"syncStatus": "queued",
"message": "PostHog event history import queued. It restarts from the beginning and re-imported events dedupe, so nothing is duplicated. Poll the integration to watch syncStatus and lastSyncAt."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Supabase user sync has no table configured. Pick the project, schema, and table once in Settings -> Integrations (Supabase); after that this backfill is re-runnable from the API."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Connect PostHog with a personal API key, region, and project ID before importing history."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Connect Segment with a Unify space ID and Profile API token before importing history."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Shopify does not support a manual sync from the API. Re-run the data sync from Settings -> Integrations in the dashboard. If you are trying to turn on storefront tracking (product views, cart activity, browse abandonment), use POST /v1/integrations/{id}/pixel instead - that part is API-triggerable."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
```json 409 theme={null}
{
"error": "A sync is already in progress for this integration."
}
```
```json 409 theme={null}
{
"success": false,
"error": "A PostHog history import is already in progress. Wait for it to finish."
}
```
```json 409 theme={null}
{
"success": false,
"error": "A Segment history import is already in progress. Wait for it to finish."
}
```
# Update Integration Sync and List Targeting
Source: https://docs.sequenzy.com/api-reference/integrations/update
PATCH /api/v1/integrations/{id}
Control an integration's bulk sync and which lists its new contacts join
Control what a connected integration does to your contact list. Two independent
settings, and neither one is a stop switch:
| Field | What it controls | What it does NOT do |
| - | - | - |
| `syncEnabled` | Bulk imports and backfills | Does not stop the provider's live webhook creating contacts |
| `listIds` | Which lists the integration's contacts join | Does not stop contacts being created, and never removes anyone from a list |
`listIds` changes list membership only. Contacts are still created, their
attributes still sync, sync-rule tags still apply, and default `any_contact`
sequences still enroll them. Explicit `any_list` and specific-list sequences
require a matching membership and do not enroll a list-less contact. To stop
`any_contact` enrollments too, pause enrollments on the affected sequences.
Webflow is fail-closed: if no native forms are selected, capture is off and no
contacts are created. This endpoint can change only the explicit list IDs for
future accepted submissions; form selection, retained fields, required double
opt-in, and sequence enrollment remain dashboard-only.
Retargeting takes effect on future provider writes. Nothing is applied
retroactively and nobody is ever removed from a list. Wix or Webflow
submissions, Shopify customer updates, and Supabase resubscriptions can add an
existing contact to the new targets. Stripe is creation-only: its targeting
applies when a webhook first creates the subscriber. A plain Supabase row
update goes through profile sync and does not touch lists.
To stop a provider feeding the workspace entirely, disconnect the integration
using [Disconnect Integration](/api-reference/integrations/disconnect) for
Lemon Squeezy. Other providers disconnect from Settings → Integrations in the
dashboard, which runs their provider-specific cleanup.
Requires an API key with the `integrations:manage` scope. Agent-safe keys do not
carry it, by design: an agent can read the current state from
[Get Integration](/api-reference/integrations/get) and report the fix, but
applying it needs a key with integration management rights.
The provider catalog's `actions` field identifies which controls a provider
exposes: `enable_sync`/`disable_sync` and `set_list_targeting`. List targeting is
honored by Supabase, Stripe, Shopify, Wix, and Webflow. A sync already in
progress must finish before bulk sync can be disabled.
## Request
Integration ID, from [List Integrations](/api-reference/integrations/list).
`true` to enable bulk imports and backfills, `false` to pause them.
Lists that contacts created by this integration join, applied from the
provider's next write onward. `null` clears the choice so they follow the
workspace default lists, `[]` means they join no list at all, and a populated
array means exactly those lists. Every ID must belong to this company. Webflow
is the exception: it rejects `null` and requires `[]` or explicit list IDs so
capture cannot inherit an undisclosed audience. Configure its forms, allowed
fields, required double opt-in, and sequence enrollment in the dashboard.
At least one field is required.
```bash theme={null}
# Stop a Supabase integration landing new signups on your marketing lists
curl -X PATCH "https://api.sequenzy.com/api/v1/integrations/int_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"listIds": []}'
```
```bash theme={null}
# Or send them to one specific list instead of the workspace defaults
curl -X PATCH "https://api.sequenzy.com/api/v1/integrations/int_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"listIds": ["list_signups"]}'
```
## Response fields
`false` when the integration was already in the requested state. Setting the
current state is not an error, so a retry is safe.
Which controls actually moved.
`company_default`, `none`, or `specific`. `null` for providers that ignore
per-integration list targeting, which you can reach by toggling `syncEnabled`
on a sync-capable provider such as Polar.
The configured target lists. `null` means new contacts follow the workspace
default lists, and is what a clear-to-default call returns.
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_abc123",
"provider": "supabase",
"syncEnabled": true,
"listTargeting": "none",
"listIds": [],
"changed": true,
"changedFields": ["listIds"],
"message": "New contacts join no lists."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Polar does not honor per-integration list targeting. Contacts it creates follow the workspace default lists."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Integration not found"
}
```
```json 409 theme={null}
{
"error": "A sync is already in progress for this integration. Wait for it to finish before disabling bulk sync."
}
```
# API Reference
Source: https://docs.sequenzy.com/api-reference/introduction
Complete API reference for Sequenzy
The Sequenzy API lets you programmatically manage subscribers, send emails, trigger events, and integrate email marketing into your application.
## Base URL
All API requests are made to:
```
https://api.sequenzy.com/api/v1
```
For backwards compatibility, requests to `https://api.sequenzy.com/v1` are still accepted as an alias.
## Authentication
All endpoints require an API key in the `Authorization` header:
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
Get your API key from [the Sequenzy dashboard](https://www.sequenzy.com/dashboard). After you sign in, go to **Settings → API Keys & SMTP**.
Learn more about API authentication
For agentic API or MCP access, use the user-approved flow in [auth.md](https://www.sequenzy.com/auth.md). This is separate from normal dashboard account signup. Agents that need dashboard-session setup notes can read [agents.md](https://www.sequenzy.com/agents.md).
## Response Format
All responses return JSON with a consistent structure.
### Success Response
```json theme={null}
{
"success": true,
"data": { ... }
}
```
### Error Response
```json theme={null}
{
"success": false,
"error": "Human readable error message"
}
```
## HTTP Status Codes
| Status | Description |
| - | - |
| `200` | Success |
| `400` | Bad request (validation error, missing fields, disabled resource) |
| `401` | Unauthorized (invalid or missing API key) |
| `404` | Resource not found |
| `409` | Conflict (resource already exists) |
| `422` | A field has the wrong type or an unsupported value |
| `429` | API rate limit exceeded |
| `503` | Temporarily unavailable; retry as directed |
| `500` | Internal server error |
## Available Endpoints
### Subscribers
Manage your subscriber list.
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/subscribers` | Create subscriber |
| `GET` | `/subscribers` | List subscribers |
| `GET` | `/subscribers/:email` | Get subscriber |
| `PATCH` | `/subscribers/:email` | Update subscriber |
| `DELETE` | `/subscribers/:email` | Delete subscriber |
| `GET` | `/subscribers/:email/notes` | List subscriber notes |
| `POST` | `/subscribers/:email/notes` | Create subscriber note |
### Tags
Segment subscribers with tags.
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/subscribers/tags` | Add tag to subscriber |
| `POST` | `/subscribers/tags/bulk` | Add multiple tags |
### Events
Track subscriber actions and trigger automations.
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/subscribers/events` | Trigger event |
| `POST` | `/subscribers/events/bulk` | Trigger multiple events |
### Sequences
Create and manage automation sequences.
| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/sequences` | List sequences |
| `POST` | `/sequences` | Create sequence |
| `GET` | `/sequences/:sequenceId` | Get sequence |
| `PUT` | `/sequences/:sequenceId` | Update sequence |
| `POST` | `/sequences/:sequenceId/enable` | Enable sequence |
| `POST` | `/sequences/:sequenceId/disable` | Disable sequence |
| `DELETE` | `/sequences/:sequenceId` | Delete sequence |
### Generation
Generate draft email content for review.
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/generate/email` | Generate a draft email |
| `POST` | `/generate/sequence` | Deprecated alias that creates a disabled sequence draft |
| `POST` | `/generate/subjects` | Generate subject line ideas |
### Transactional Emails
Send programmatic emails.
| Method | Endpoint | Description |
| - | - | - |
| `POST` | `/transactional/send` | Send email |
| `GET` | `/transactional` | List templates |
| `GET` | `/transactional/:slug` | Get template details |
## Auto-Creation Behavior
The API automatically creates resources when they don't exist:
| Resource | When Created |
| - | - |
| **Subscribers** | When adding tags, triggering events, or sending transactional emails to a new email |
| **Tags** | When adding a tag that doesn't exist |
| **Events** | When triggering an event type that doesn't exist |
This makes integration seamless: you don't need to pre-configure anything in the dashboard.
### Example
```bash theme={null}
# This single request will:
# 1. Create the subscriber (if new)
# 2. Create the "customer" tag (if new)
# 3. Add the tag to the subscriber
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new-user@example.com",
"tag": "customer"
}'
```
## Rate Limiting
Authenticated requests to both `/api/v1` and the legacy `/v1` alias are rate
limited per API key across all API servers:
* **Standard limit**: 300 requests per minute per API key
* **Burst limit**: 20 requests per second
Both limits apply at the same time. The canonical and legacy paths share one
budget, so changing paths does not reset it. When rate limited, you'll receive
a `429 Too Many Requests` response with `Retry-After`, `RateLimit-Limit`,
`RateLimit-Remaining`, `RateLimit-Reset`, and `RateLimit-Policy` headers. Public
form and widget submissions use their own abuse limits instead.
`POST /api/v1/transactional/send` has its own budget of 1,000 requests per
minute and 50 per second per API key, so a burst of order confirmations or a
daily reminder run does not consume the standard budget your other calls need.
Each send is one request per recipient; if you expect sustained volume above
this, contact support. For bulk contact work use the
[subscriber import](/api-reference/subscribers/import-create) endpoint rather
than one create request per contact.
The remote MCP endpoint (`/v1/mcp`) has its own budget of 300 requests per
minute and 20 per second per API key, so MCP protocol traffic does not spend
your REST budget. The API calls your MCP tool calls make still count against
the standard limit above. An MCP request that is rate limited returns a
JSON-RPC error rather than the REST error envelope.
## Pagination
List endpoints support pagination:
| Parameter | Type | Default | Description |
| - | - | - | - |
| `page` | integer | 1 | Page number |
| `limit` | integer | 20 | Items per page (max 100) |
### Example
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers?page=2&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Response
```json theme={null}
{
"success": true,
"data": [...],
"pagination": {
"page": 2,
"limit": 50,
"total": 1250,
"totalPages": 25
}
}
```
## Error Handling
### Retrying Temporary Database Failures
When a temporary database connection failure prevents an idempotent request
from completing, the API returns `503 Service Unavailable` with the stable error
code `SERVICE_UNAVAILABLE`. The `Retry-After` response header specifies how many
seconds to wait before trying the same request again.
Automatic retries are advertised only for `GET`, `HEAD`, `OPTIONS`, `PUT`, and
`DELETE` requests. The API does not attach this recovery signal to `POST` or
`PATCH` failures because a disconnected write may already have committed and
replaying it could apply the change twice.
```json theme={null}
{
"success": false,
"error": "The service could not reach its database for a moment. Retry the request.",
"code": "SERVICE_UNAVAILABLE"
}
```
### Field Type Errors
When a field has the wrong type or a value outside its allowed set, the API
returns `422`. `property` points to the first failing field and `summary`
explains it. `errors` lists up to 10 failures, with `allowedValues` when a field
accepts a fixed set of values. `found` echoes the request part that failed.
```json theme={null}
{
"type": "validation",
"on": "body",
"property": "/enrollmentMode",
"message": "Expected union value",
"summary": "Property 'enrollmentMode' must be one of: unlimited, one_time, matching_field",
"found": { "name": "Welcome series", "enrollmentMode": "once" },
"errors": [
{
"path": "/enrollmentMode",
"message": "Expected union value",
"summary": "Property 'enrollmentMode' must be one of: unlimited, one_time, matching_field",
"allowedValues": ["unlimited", "one_time", "matching_field"]
}
]
}
```
A query parameter outside its numeric range, such as `limit=0`, reports
`"on": "property"` and `"property": "root"` with the rejected value in `found`.
Fix the named field and retry. Some endpoints report input problems as `400`
with an `error` message instead.
### Common Error Codes
| Error | Description | Solution |
| - | - | - |
| `Unauthorized` | Invalid or missing API key | Check your API key |
| `Subscriber not found` | Email doesn't exist | Create the subscriber first |
| `Template not found` | Invalid template slug | Check the slug spelling |
| `Template disabled` | Template is deactivated | Enable in dashboard |
| `Validation error` | Missing or invalid fields | Check request body |
### Example Error Response
```json theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
## SDK & Libraries
### Node.js / TypeScript SDK (Recommended)
Install the official Sequenzy SDK for the best developer experience:
```bash theme={null}
npm install sequenzy
# or
pnpm add sequenzy
# or
bun add sequenzy
```
```typescript theme={null}
import Sequenzy from "sequenzy";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY,
});
// Create a subscriber
await client.subscribers.create({
email: "user@example.com",
firstName: "John",
tags: ["new-signup"],
customAttributes: { plan: "pro" },
enrollInSequences: true,
});
// Add a tag
await client.subscribers.tags.add({
email: "user@example.com",
tag: "customer",
});
// Trigger an event
await client.subscribers.events.trigger({
email: "user@example.com",
event: "purchase_completed",
properties: { orderId: "ORD-123", amount: 99.99 },
});
// Send a transactional email
await client.transactional.send({
to: "user@example.com",
slug: "welcome-email",
variables: { firstName: "John" },
});
```
### Python, PHP, and Go SDKs
Official SDKs are also available for Python, PHP, and Go, generated from the
same OpenAPI spec as the API reference:
```bash theme={null}
pip install sequenzy # Python
composer require sequenzy/sequenzy # PHP
go get github.com/sequenzy/sequenzy-go # Go
```
Each client mirrors the TypeScript SDK's structure - resource groups with
short methods (`subscribers.create`, `campaigns.list`,
`transactional.send`) and typed request/response models.
### TypeScript/JavaScript (Fetch API)
If you prefer to use the REST API directly:
```javascript theme={null}
async function addTag(email, tag) {
const response = await fetch(
"https://api.sequenzy.com/api/v1/subscribers/tags",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, tag }),
}
);
return response.json();
}
```
### Python
```python theme={null}
import requests
def add_tag(email, tag):
response = requests.post(
"https://api.sequenzy.com/api/v1/subscribers/tags",
headers={
"Authorization": f"Bearer {SEQUENZY_API_KEY}",
"Content-Type": "application/json"
},
json={"email": email, "tag": tag}
)
return response.json()
```
### cURL
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "tag": "customer"}'
```
## Need Help?
* **Email**: [nic@sequenzy.com](mailto:nic@sequenzy.com)
* **Twitter**: [@nikpolale](https://x.com/nikpolale)
# Connect Landing Page Domain
Source: https://docs.sequenzy.com/api-reference/landing-pages/connect-domain
POST /api/v1/landing-pages/domain
Connect a custom domain for landing pages
Connect a custom domain for published landing pages. After connecting, add the
returned DNS record and verify the domain: subdomains use a CNAME record
pointing to `pages.sequenzydns.com`, root domains use an A record pointing to
`76.76.21.21`. When you connect a root domain, its `www` host also redirects to
the root, provided the `www` host has a CNAME record pointing to
`pages.sequenzydns.com` and is not connected as its own landing page domain.
The `dnsRecordType`, `dnsRecordName`, and `dnsRecordValue` response fields
describe the record to add.
The route above configures the backward-compatible workspace domain shared by
all pages. To dedicate a hostname to one page and serve it from the hostname
root, use `POST /api/v1/landing-pages/{landingPageId}/domain`. Existing
workspace and Sequenzy URLs remain available.
## Request
Custom landing page domain, for example `pages.example.com` or a root domain
like `example.com`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/domain" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "pages.example.com"}'
```
```bash Dedicated page domain theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/domain" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "offer.example.com"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": "pages.example.com",
"domainStatus": "pending",
"verifiedAt": null,
"dnsRecordAddedAt": null,
"sslStatus": null,
"error": null,
"verificationRecords": [],
"lastCheckedAt": "2026-05-01T10:30:00Z",
"cnameTarget": "pages.sequenzydns.com",
"dnsRecordType": "CNAME",
"dnsRecordName": "pages.example.com",
"dnsRecordValue": "pages.sequenzydns.com"
},
"message": "Landing page domain connected. Add a CNAME record pointing to pages.sequenzydns.com, then verify the domain."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Enter a valid domain like pages.acme.com."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "This landing page domain is already connected."
}
```
# Landing Page Content
Source: https://docs.sequenzy.com/api-reference/landing-pages/content
Build and edit landing pages with version 2 content, blocks, themes, and signup forms
Use this document as the `content` field when [creating](/api-reference/landing-pages/create)
or [updating](/api-reference/landing-pages/update) a page. To change an existing
page, read its `content`, modify it, and send the complete document. Updating
content replaces the document; changes to a published page are immediately live.
## Minimal content
```json theme={null}
{
"version": 2,
"template": "from-scratch",
"seo": { "title": "Product updates" },
"header": { "enabled": false },
"theme": { "accentColor": "#0f766e" },
"blocks": [
{
"id": "headline",
"kind": "heading",
"slot": "hero",
"level": 1,
"content": "Stay in the loop"
},
{
"id": "signup",
"kind": "form",
"slot": "form",
"heading": "Get product updates",
"form": {
"listMode": "default",
"buttonText": "Subscribe"
}
},
{
"id": "footer",
"kind": "footer",
"slot": "footer",
"brandText": "Acme"
}
]
}
```
`version: 2` and a `blocks` array are required. An empty array is accepted;
Sequenzy appends its default footer when no footer is present. Omitted `template`,
`seo`, `header` and `theme` settings receive defaults. Valid templates are
`from-scratch`, `waitlist`, `lead-magnet`, `launch`, `demo-request`, `webinar`,
`newsletter`, `product-hunt`, `pricing-offer`, `agency-lead-gen`, and
`feature-announcement`; the default is `waitlist`.
Every block needs a unique `id` (1-128 characters), a `kind`, and a `slot`.
Slots render in this order: `top`, `hero`, `form`, `body`, `footer`. Preserve
that order when authoring the top-level array. You can supply one footer to customize it, or omit it to use the default.
After normalization the page has exactly one footer and at most one form;
both must stay at the page root. IDs must also be unique
inside groups. Text fields are trimmed.
## Shared block fields
Blocks may include `sectionId` (1-128 characters), `sectionKind` (1-80),
`sectionLabel` (1-120), `sectionVariant` (up to 80), `sectionLayout` (`stack`,
`split`, `center`), and `sectionAlign` (`left`, `center`, `right`).
An optional block-level `align` (`left`, `center`, `right`) is supported by
heading, text, image, video, button, feature-grid, stats, testimonial,
logo-cloud, FAQ and pricing blocks.
## Block kinds
Required fields below are in addition to `id`, `kind` and `slot`. Fields with
a default may be omitted.
| Kind | Required fields | Optional fields and defaults |
| - | - | - |
| `heading` | `content` (1-180 characters) | `level`: 1, 2 or 3, default 2; `align` |
| `text` | `content` (up to 700 characters) | `variant`: `paragraph` (default), `eyebrow` or `caption`; `align` |
| `image` | None | `src` (up to 1,000 characters) and `alt` (up to 160), both default empty; `images` (up to 3 gallery entries); `align`; `width` (25-100 percent); `height` (80-640 pixels); `fit` (`cover` or `contain`) |
| `video` | None | `url` (YouTube share URL, up to 1,000 characters), `title` (up to 160), both default empty; `aspectRatio`: `16:9` (default), `4:3`, `1:1` or `9:16`; `width` (25-100 percent); `align` |
| `button` | `text` (1-80 characters) | `url` (up to 500 characters, default empty); `variant`: `primary` (default) or `secondary`; `align` |
| `custom-html` | None | `html` (up to 20,000 characters, default empty); `height` (40-1,200 pixels, default 320) |
| `feature-grid` | `features` (1-6 entries) | `columns` (1-6); `layout` (`row` or `column`); `align` |
| `stats` | `stats` (1-6 entries) | `columns` (1-6); `align` |
| `testimonial` | `testimonials` (1-3 entries) | `columns` (1-6); `layout` (`row` or `column`); `align` |
| `logo-cloud` | `logos` (1-8 entries) | `heading` (up to 120 characters, default `Trusted by teams building faster`); `columns` (1-6); `align` |
| `faq` | `items` (1-8 entries) | `heading` (up to 120 characters, default `Common questions`); `align` |
| `pricing` | `heading` (1-140 characters), `price` (1-80), `buttonText` (1-80), `features` (1-8 strings, each 1-160) | `eyebrow` (up to 80, default `Simple pricing`); `description` (up to 300), `period` (up to 80), and `buttonUrl` (up to 500), all default empty; `align` |
| `form` | `slot: "form"` | `heading` (up to 120 characters, default `Get early access`); `description` (up to 240, default empty); `form` settings below; `cardTilt` (integer -3 to 3 degrees); `cardWidth` (`narrow`, `medium`, `wide`) |
| `footer` | `slot: "footer"` | `brandText` (up to 120 characters, default `Built by Sequenzy`); `showBrandText`, `showPrivacyLink`, `showTermsLink` (default true); `privacyLabel`/`termsLabel` (up to 80, defaults `Privacy`/`Terms`); `privacyUrl`/`termsUrl` (up to 500, default empty) |
| `spacer` | None | `height` (8-160 pixels, default 32) |
| `divider` | None | Shared section fields only |
| `group` | None | `label` (1-120 characters, default `Group`); `layout`: `stack` (default), `row`, `grid` or `overlay`; `columns` (1-4, default 2); `gap` (0-64 pixels, default 16); `padding` (0-64 pixels, default 0); `children` (up to 100 blocks, default empty); overlay settings below |
Numeric layout fields in this table are integers. Nested collection entries use
these shapes:
| Collection | Entry fields |
| - | - |
| `images` | `src` (up to 1,000 characters, default empty), `alt` (up to 160, default empty), optional `linkUrl` (up to 500) |
| `features` on feature-grid | `title` (up to 120 characters), `description` (up to 300) |
| `stats` | `value` (1-40 characters), `label` (1-120) |
| `testimonials` | `quote` (1-500 characters), `name` (1-120), optional `role` (up to 160, default empty) and `avatarUrl` (up to 1,000) |
| `logos` | `name` (1-80 characters) |
| `items` on FAQ | `question` (1-160 characters), `answer` (1-500) |
Button `url` and pricing `buttonUrl` support HTTPS URLs and anchors such as
`#form`, `#section-`, `#block-`, and `#top`. Anchors open in
the same tab. Video blocks support YouTube, not other providers or direct files.
### Groups
Groups may nest up to eight levels. Every child must use its parent's slot;
form and footer blocks cannot be children. Overlay settings are `overlayColor`
(six-digit hex, default `#000000`), `overlayPosition` (`top`, `center`, `bottom`,
default `center`) and `overlayShade` (0-100, default 35).
An overlay requires exactly one direct image block with at most one gallery
image. Otherwise its layout normalizes to `stack`. An image inside another
nested group does not count as the parent's background.
## Form settings
Set these fields inside a form block's `form` object:
| Field | Values and default |
| - | - |
| `enabled` | Boolean, default true |
| `duplicateStrategy` | `skip` (default), `merge`, `overwrite` |
| `listMode` | `default` uses the workspace's default lists; `none` joins no lists; `specific` uses `listIds` |
| `listIds`, `tagIds` | Arrays of IDs belonging to your company, default empty |
| `showFirstName`, `showLastName`, `showPhone` | Booleans, defaults true, false, false |
| `firstNameRequired`, `lastNameRequired`, `phoneRequired` | Booleans, all default false |
| `emailPlaceholder`, `firstNamePlaceholder`, `lastNamePlaceholder`, `phonePlaceholder` | Up to 120 characters; defaults `you@example.com`, `First name`, `Last name`, `Phone number` |
| `buttonText` | 1-80 characters, default `Join the list` |
| `successMessage` | 1-240 characters, default `You're on the list.` |
| `redirectUrl` | Optional HTTP/HTTPS success redirect, up to 500 characters |
| `customFields` | Up to 8 custom subscriber fields, default empty |
Custom fields require a unique `name` (1-80 characters, starting with a letter,
then letters, numbers, underscores, dots or dashes). `email`, `firstName`,
`lastName` and `website` are reserved. Prefer the native phone field over a
custom field named `phone`.
A custom field also accepts `label` and `placeholder` (up to 120 characters,
default empty), `required` (default false), and `inputType`: `text` (default),
`phone`, `number`, `textarea`, `select`, `radio`, `checkbox`, `consent` or
`hidden`. Choice fields use `options` (up to 20 `{ label, value }` objects, each
string 1-120 characters). Consent fields use `consentText` (up to 300
characters); hidden fields use `defaultValue` (up to 500). Both default empty.
## Theme, header and SEO
Theme colors (`accentColor`, `backgroundColor`, `textColor`, `mutedTextColor`,
`cardColor`) must be six-digit hex values. Their defaults are `#f97316`,
`#ffffff`, `#111827`, `#6b7280`, and `#ffffff`, respectively.
| Theme field | Values and default |
| - | - |
| `borderRadius` | Integer 0-24 pixels, default 8 |
| `fontPair` | `modern-sans` (default), `editorial-serif`, `classic-serif`, `friendly-rounded`, `commerce-sans`, `technical-mono` |
| `headingFontFamily`, `bodyFontFamily` | Up to 160 characters, default empty |
| `density` | `compact`, `balanced` (default), `spacious` |
| `accentStyle` | `pill` (default), `underline`, `bar`, `badge` |
| `surfaceStyle` | `soft` (default), `outlined`, `solid`, `minimal` |
| `sectionAnimation` | `none` (default), `fade`, `slide-up`, `zoom-in` |
| `sectionAnimationSpeed` | `slow`, `normal` (default), `fast` |
Scroll animation is skipped when the visitor prefers reduced motion.
`header` accepts `enabled`, `showLogo`, and `showName` (all default true),
`logoUrl` (up to 1,000 characters, default empty), `logoSize` (integer 24-96
pixels, default 36), and `name` (up to 120 characters, default empty).
`seo` accepts optional `title` (up to 70 characters) and `description` (up to
160\), `faviconUrl` (up to 1,000, default empty, falling back to the company
logo), and `hideFromSearchEngines` (default false). Setting the last field adds
`noindex, nofollow`; visitors can still open the page by URL.
# Create Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/create
POST /api/v1/landing-pages
Create a draft landing page
Create a draft landing page. Provide `content` to control the builder JSON, or
omit it and choose a `template` to start from default content.
See the [content reference](/api-reference/landing-pages/content) for supported
block kinds, theme, form settings and a minimal valid builder document.
## Request
Landing page name. Defaults to a template-specific name.
URL slug. It is normalized and made unique for the company.
Optional template key, such as `from-scratch`, `waitlist`, `lead-magnet`,
`launch`, `demo-request`, `webinar`, `newsletter`, `product-hunt`,
`pricing-offer`, `agency-lead-gen`, or `feature-announcement`.
Landing page builder JSON. Form redirect URLs are validated before saving.
Button and pricing CTA URLs accept an https URL or an in-page anchor such as
`#form`, which scrolls to the page's form block. Set `theme.sectionAnimation`
(`none`, `fade`, `slide-up`, `zoom-in`) and `theme.sectionAnimationSpeed`
(`slow`, `normal`, `fast`) for a scroll reveal.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Waitlist",
"slug": "product-waitlist",
"template": "waitlist"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_abc123",
"companyId": "comp_abc123",
"name": "Product Waitlist",
"slug": "product-waitlist",
"status": "draft",
"content": {
"version": 2,
"template": "waitlist",
"blocks": [
{ "id": "hero", "kind": "heading", "slot": "hero", "level": 1, "content": "Product Waitlist" },
{ "id": "signup", "kind": "form", "slot": "form", "heading": "Get early access" },
{ "id": "footer", "kind": "footer", "slot": "footer", "brandText": "Acme" }
]
},
"viewCount": 0,
"conversionCount": 0,
"publishedAt": null,
"createdAt": "2026-05-01T10:00:00Z",
"updatedAt": "2026-05-01T10:00:00Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages/lp_abc123",
"appPublicUrl": null,
"publicUrl": null,
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
},
"message": "Landing page created as a draft. Use render_landing_page to preview it without publishing, then publish_landing_page when it is ready to go live."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid landing page content."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create landing page"
}
```
# Delete Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/delete
DELETE /api/v1/landing-pages/{landingPageId}
Delete a landing page
Delete a landing page.
## Request
Landing page ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPageId": "lp_abc123",
"message": "Landing page deleted."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unpublish the landing page before deleting it."
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Duplicate Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/duplicate
POST /api/v1/landing-pages/{landingPageId}/duplicate
Copy an existing landing page into a new draft
Duplicate a landing page instead of rebuilding it from scratch. The copy is
always created as a draft with its own slug, views, and conversions, so the
original keeps its published URL and stats.
## Request
ID of the landing page to copy.
Name for the copy. Defaults to the original name with a `(copy)` suffix.
URL slug for the copy. It is normalized and made unique within the company.
Defaults to a slug generated from the copy's name.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/duplicate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring Sale v2", "slug": "spring-sale-v2"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_def456",
"name": "Spring Sale v2",
"slug": "spring-sale-v2",
"status": "draft",
"viewCount": 0,
"conversionCount": 0,
"appPublicUrl": null,
"publicUrl": null,
"previewUrl": "https://sequenzy.com/lp/preview/lp_def456?token=..."
},
"message": "Landing page duplicated as a draft. Use render_landing_page to preview the copy, then publish_landing_page when it is ready to go live."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Landing page name cannot be empty."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Get Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/get
GET /api/v1/landing-pages/{landingPageId}
Get landing page details
Get one landing page, including its builder content, public URLs, and a signed
`previewUrl` that shows the visitor-facing page even while it is still a draft.
## Request
Landing page ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_abc123",
"companyId": "comp_abc123",
"name": "Product Waitlist",
"slug": "product-waitlist",
"status": "published",
"content": { "version": 2, "template": "waitlist", "blocks": [] },
"viewCount": 1240,
"conversionCount": 186,
"publishedAt": "2026-05-01T10:30:00Z",
"createdAt": "2026-05-01T10:00:00Z",
"updatedAt": "2026-05-01T10:30:00Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages/lp_abc123",
"appPublicUrl": "https://sequenzy.com/lp/comp_abc123/product-waitlist",
"publicUrl": "https://pages.example.com/product-waitlist",
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Get Landing Page Domain
Source: https://docs.sequenzy.com/api-reference/landing-pages/get-domain
GET /api/v1/landing-pages/domain
Get the custom landing page domain settings
Returns the custom domain used for your published landing pages, along with its DNS and SSL status.
The `domain` object is always returned. When no custom domain is connected, its `domain` field is `null` and `domainStatus` is `not_started`.
To inspect a hostname assigned to just one page, use [Get Dedicated Page
Domain](/api-reference/landing-pages/get-page-domain). You can [remove that
assignment](/api-reference/landing-pages/remove-page-domain) while retaining
the shared workspace domain.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/landing-pages/domain" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": "pages.example.com",
"domainStatus": "verified",
"verifiedAt": "2026-05-01T10:35:00Z",
"dnsRecordAddedAt": "2026-05-01T10:00:00Z",
"sslStatus": "active",
"error": null,
"verificationRecords": [],
"lastCheckedAt": "2026-05-01T10:35:00Z",
"cnameTarget": "pages.sequenzydns.com"
}
}
```
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": null,
"domainStatus": "not_started",
"verifiedAt": null,
"dnsRecordAddedAt": null,
"sslStatus": null,
"error": null,
"verificationRecords": [],
"lastCheckedAt": null,
"cnameTarget": "pages.sequenzydns.com"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Get Dedicated Page Domain
Source: https://docs.sequenzy.com/api-reference/landing-pages/get-page-domain
GET /api/v1/landing-pages/{landingPageId}/domain
Read one landing page's dedicated hostname and workspace fallback
Read the hostname assigned to one page. This is separate from the shared
[workspace domain](/api-reference/landing-pages/get-domain). Requires
`landing_pages:read` and access to the page's company.
Landing page ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/domain" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The `domain` object is returned even when no dedicated hostname exists. In that
case `domain.domain` is `null` and `domainStatus` is `not_started`.
`fallbackDomain` and `fallbackDomainStatus` describe the shared workspace
domain. Use `dnsRecordType`, `dnsRecordName` and `dnsRecordValue` when showing DNS
instructions; root hostnames use an A record while subdomains use a CNAME.
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": null,
"domainScope": "page",
"landingPageId": "lp_abc123",
"domainStatus": "not_started",
"fallbackDomain": "pages.example.com",
"fallbackDomainStatus": "verified",
"verifiedAt": null,
"dnsRecordAddedAt": null,
"sslStatus": null,
"error": null,
"verificationRecords": [],
"lastCheckedAt": null,
"cnameTarget": "pages.sequenzydns.com",
"dnsRecordType": "CNAME",
"dnsRecordName": null,
"dnsRecordValue": "pages.sequenzydns.com"
}
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: landing_pages:read" }
```
```json 404 theme={null}
{ "error": "Landing page not found" }
```
Connect or verify a hostname through [Connect Landing Page Domain](/api-reference/landing-pages/connect-domain)
and [Verify Landing Page Domain](/api-reference/landing-pages/verify-domain).
# List Landing Pages
Source: https://docs.sequenzy.com/api-reference/landing-pages/list
GET /api/v1/landing-pages
List landing pages for the current company
List landing pages for the authenticated company.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/landing-pages" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"companyId": "comp_abc123",
"landingPages": [
{
"id": "lp_abc123",
"companyId": "comp_abc123",
"name": "Product Waitlist",
"slug": "product-waitlist",
"status": "published",
"content": { "version": 2, "template": "waitlist", "blocks": [] },
"viewCount": 1240,
"conversionCount": 186,
"publishedAt": "2026-05-01T10:30:00Z",
"createdAt": "2026-05-01T10:00:00Z",
"updatedAt": "2026-05-01T10:30:00Z",
"url": "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages/lp_abc123",
"appPublicUrl": "https://sequenzy.com/lp/comp_abc123/product-waitlist",
"publicUrl": "https://pages.example.com/product-waitlist",
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
}
],
"url": "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Publish Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/publish
POST /api/v1/landing-pages/{landingPageId}/publish
Publish a landing page
Publish a landing page. You can optionally update its name, slug, or content in
the same request.
## Request
Landing page ID.
Optional updated landing page name.
Optional updated URL slug.
Optional updated landing page builder JSON.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/publish" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_abc123",
"name": "Product Waitlist",
"slug": "product-waitlist",
"status": "published",
"publishedAt": "2026-05-01T10:30:00Z",
"appPublicUrl": "https://sequenzy.com/lp/comp_abc123/product-waitlist",
"publicUrl": "https://pages.example.com/product-waitlist",
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid landing page content."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Remove Dedicated Page Domain
Source: https://docs.sequenzy.com/api-reference/landing-pages/remove-page-domain
DELETE /api/v1/landing-pages/{landingPageId}/domain
Remove a page-specific hostname while keeping fallback URLs available
Remove only this page's dedicated hostname. The shared workspace domain and
Sequenzy URL remain available for published pages. Requires `landing_pages:delete`
and access to the page's company.
Landing page ID. No request body is needed.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/domain" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The response returns the same `domain` shape as [Get Dedicated Page Domain](/api-reference/landing-pages/get-page-domain),
with `domain: null`, `domainStatus: "not_started"` and the workspace fallback
fields. Removing a page's already absent domain also succeeds.
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": null,
"domainScope": "page",
"landingPageId": "lp_abc123",
"domainStatus": "not_started",
"fallbackDomain": "pages.example.com",
"fallbackDomainStatus": "verified",
"verifiedAt": null,
"dnsRecordAddedAt": null,
"sslStatus": null,
"error": null,
"verificationRecords": [],
"lastCheckedAt": null,
"cnameTarget": "pages.sequenzydns.com",
"dnsRecordType": "CNAME",
"dnsRecordName": null,
"dnsRecordValue": "pages.sequenzydns.com"
},
"message": "Page domain removed."
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: landing_pages:delete" }
```
```json 404 theme={null}
{ "error": "Landing page not found" }
```
```json 500 theme={null}
{ "error": "Landing page domain setup is temporarily unavailable." }
```
# Render Landing Page Preview
Source: https://docs.sequenzy.com/api-reference/landing-pages/render
POST /api/v1/landing-pages/{landingPageId}/render
Get a visitor-facing preview URL for a landing page without publishing it
Return a signed, unlisted preview of the current landing page content. Use this
to check layout, copy, form placement, and in-page anchors such as `#form`
before the page is live.
Drafts keep `publicUrl` and `appPublicUrl` set to `null` until you publish.
`previewUrl` works for drafts and published pages. It is not indexed, does not
count as a public view, and signup forms on a draft preview do not collect
contacts.
This endpoint is read-only. It requires the `landing_pages:read` scope and
never publishes, unpublishes, or modifies the page. It uses `POST` to match
the other render endpoints.
## Request
Landing page ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/render" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPageId": "lp_abc123",
"name": "Product Waitlist",
"status": "draft",
"published": false,
"title": "Join the Acme waitlist",
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=...",
"publicUrl": null,
"message": "Draft preview. This URL is not indexed and is not on your public domain. Signup forms do not collect contacts until you publish. Open previewUrl to review the page as visitors will see it."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "Landing page not found"
}
```
# Get Landing Page Stats
Source: https://docs.sequenzy.com/api-reference/landing-pages/stats
GET /api/v1/landing-pages/{landingPageId}/stats
Get visits, unique visits, clicks, subscribes, conversion rate, referrers, and a daily histogram
Mailchimp-style landing page analytics for one page. Default totals exclude
known crawlers. Preview URLs and the editor never count. Opening the public
URL while building the page does count.
`conversionRate` is subscribes divided by unique visits, as a percentage.
The histogram is daily; `period=all` returns all retained analytics and a
90-day chart. `dataAvailableFrom` identifies the beginning of that retained
coverage. Events from before analytics collection began are not backfilled,
and raw events expire after two years. Lifetime `viewCount` / `conversionCount`
on the page remain PostgreSQL compatibility counters for lists and
`get_landing_page`, so they may include earlier traffic.
## Request
Landing page ID.
Time window: `7d`, `30d`, `90d`, or `all`. Defaults to `all`.
Custom range start as an ISO 8601 timestamp. Provide `end` too. Maximum range
is 365 days.
Custom range end as an ISO 8601 timestamp. Provide `start` too.
Include known crawlers in visits, unique visits, clicks, referrers, and the
histogram. Crawler hits are always returned separately as `botVisits`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/stats?period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"stats": {
"landingPageId": "lp_abc123",
"period": "30d",
"start": "2026-07-31T00:00:00.000Z",
"end": "2026-08-29T12:00:00.000Z",
"dataAvailableFrom": "2026-08-29T00:00:00.000Z",
"visits": 48,
"uniqueVisits": 31,
"clicks": 12,
"subscribes": 6,
"conversionRate": 19.35,
"botVisits": 4,
"lifetimeViewCount": 52,
"lifetimeConversionCount": 6,
"histogram": [
{
"date": "2026-08-28",
"visits": 9,
"uniqueVisits": 7,
"clicks": 3,
"subscribes": 1
}
],
"referrers": [
{ "name": "twitter.com", "visits": 20, "uniqueVisits": 14 },
{ "name": "direct", "visits": 11, "uniqueVisits": 9 }
],
"sources": [
{ "name": "twitter", "visits": 18, "uniqueVisits": 12 },
{ "name": "direct", "visits": 13, "uniqueVisits": 10 }
],
"countsInclude": "Published public page loads retained from dataAvailableFrom. Known crawlers are excluded from visits, unique visits, and clicks. Preview URLs and the editor do not count. Opening the public URL while building the page does count. Lifetime page counters may include earlier traffic."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Landing page stats period must be 7d, 30d, 90d, or all."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Unpublish Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/unpublish
POST /api/v1/landing-pages/{landingPageId}/unpublish
Return a landing page to draft
Unpublish a landing page and return it to draft status. You can optionally
update its name, slug, or content in the same request.
## Request
Landing page ID.
Optional updated landing page name.
Optional updated URL slug.
Optional updated landing page builder JSON.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123/unpublish" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_abc123",
"name": "Product Waitlist",
"slug": "product-waitlist",
"status": "draft",
"appPublicUrl": null,
"publicUrl": null,
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid landing page content."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Update Landing Page
Source: https://docs.sequenzy.com/api-reference/landing-pages/update
PUT /api/v1/landing-pages/{landingPageId}
Update a draft or published landing page
Update a landing page name, slug, or builder content.
Changes to a published page take effect immediately, including content and
slug changes. This endpoint does not create a separate draft or require
another publish call. Duplicate the page to experiment without changing the
live page.
Omitted top-level fields keep their values. Providing `content` replaces the
whole builder document, not just the supplied blocks or theme keys. Read the
current page, edit its content and send the complete document. See the
[content reference](/api-reference/landing-pages/content) for the schema and a
minimal valid example.
## Request
Landing page ID.
Updated landing page name.
Updated URL slug. It is normalized and made unique for the company.
Updated landing page builder JSON. Form redirect URLs are validated before
saving. Button and pricing CTA URLs accept an https URL or an in-page anchor
such as `#form`, and `theme.sectionAnimation` controls the scroll reveal.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/landing-pages/lp_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Product Waitlist",
"slug": "updated-product-waitlist"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"landingPage": {
"id": "lp_abc123",
"name": "Updated Product Waitlist",
"slug": "updated-product-waitlist",
"status": "draft",
"publicUrl": null,
"previewUrl": "https://sequenzy.com/lp/preview/lp_abc123?token=..."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide name, slug, or content."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Landing page not found"
}
```
# Update Landing Page Domain Settings
Source: https://docs.sequenzy.com/api-reference/landing-pages/update-domain-settings
PUT /api/v1/landing-pages/domain
Update or verify landing page domain settings
Update the custom domain for landing pages, verify the current domain, or do
both in one request.
## Request
Replacement custom landing page domain.
Set to `true` to check the current domain's DNS and SSL status.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/landing-pages/domain" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "pages.example.com", "verify": true}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": "pages.example.com",
"domainStatus": "verified",
"verifiedAt": "2026-05-01T10:35:00Z",
"dnsRecordAddedAt": "2026-05-01T10:35:00Z",
"sslStatus": "issued",
"error": null,
"verificationRecords": [],
"lastCheckedAt": "2026-05-01T10:35:00Z",
"cnameTarget": "pages.sequenzydns.com",
"dnsRecordType": "CNAME",
"dnsRecordName": "pages.example.com",
"dnsRecordValue": "pages.sequenzydns.com"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide domain or verify."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "This landing page domain is already connected."
}
```
# Verify Landing Page Domain
Source: https://docs.sequenzy.com/api-reference/landing-pages/verify-domain
POST /api/v1/landing-pages/domain/verify
Verify landing page domain DNS and SSL status
Check DNS and SSL status for the current custom landing page domain.
For a dedicated page domain, call
`POST /api/v1/landing-pages/{landingPageId}/domain/verify`.
## Request
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/landing-pages/domain/verify" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"domain": {
"domain": "pages.example.com",
"domainStatus": "verified",
"verifiedAt": "2026-05-01T10:35:00Z",
"dnsRecordAddedAt": null,
"sslStatus": "active",
"error": null,
"verificationRecords": [],
"lastCheckedAt": "2026-05-01T10:35:00Z",
"cnameTarget": "pages.sequenzydns.com",
"actualValues": ["pages.sequenzydns.com"],
"verified": true,
"message": "Landing page domain verified."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Add a landing page domain first."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Landing page domain setup failed."
}
```
# Add Subscribers To List
Source: https://docs.sequenzy.com/api-reference/lists/add-subscribers
POST /api/v1/lists/{listId}/subscribers
Bulk add subscribers to a list
Add existing or new subscribers to one list from an email array. Send up to 500
email addresses per request. This is useful for temporary batch lists, imports,
and support workflows.
Use exactly `POST /api/v1/lists/{listId}/subscribers`. There is no `/bulk`
suffix for this endpoint.
Prefer this endpoint over looping one subscriber per request. Standard API rate
limits still apply: 300 requests per minute per API key and 20 requests per
second burst.
## Permissions
This endpoint needs both `lists:write` and `subscribers:write`. Membership is
contact state, so `lists:write` on its own only lets a key create and rename
lists. The Safer agent access preset includes `lists:write` but not
`subscribers:write`, so keys on that preset cannot change who is in a list.
## Request
Subscriber list ID.
Email addresses to add to the list. Maximum 500 per request.
Existing subscriber behavior: `skip`, `merge`, or `overwrite`.
Whether newly created subscribers should enter matching sequences.
Consent mode for newly created subscribers: `default`, `confirmed`, or
`double_opt_in`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/lists/list_abc123/subscribers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": ["one@example.com", "two@example.com"],
"duplicateStrategy": "skip",
"enrollInSequences": true
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"listId": "list_abc123",
"total": 2,
"processed": 2,
"created": 1,
"updated": 1,
"skipped": 0,
"addedToList": 2,
"failed": 0,
"duplicateInputCount": 0,
"ignoredBlankCount": 0,
"results": [
{
"email": "one@example.com",
"success": true,
"created": false,
"updated": false,
"skipped": true,
"addedToList": true
}
]
}
```
```json 400 theme={null}
{
"error": "Provide at least one non-empty email address."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"error": "List not found"
}
```
```json 500 theme={null}
{
"error": "Failed to add subscribers to list"
}
```
# Create Subscriber List
Source: https://docs.sequenzy.com/api-reference/lists/create
POST /api/v1/lists
Create a subscriber list
Create a subscriber list for grouping contacts.
## Request
List name.
Optional internal list description. It is visible to workspace members through
authenticated list-management surfaces, but never appears in hosted or
embedded subscriber preferences.
Private lists stay hidden from subscriber preferences unless you enable member
unsubscribe. Public lists can be discovered and joined there. Descriptions
always stay internal. Omission defaults to public on creation and preserves
visibility on update.
Allow existing members of a private list to see its name and unsubscribe in
email preferences. Defaults to `false` on creation. On update, omit to
preserve the setting or pass `false` to disable it; `null` is rejected. The
setting is stored but has no effect on public lists. Members cannot join or
rejoin private lists through preferences.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/lists" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "VIP Customers", "description": "High-value customers"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"list": {
"id": "list_abc123",
"name": "VIP Customers",
"description": "High-value customers",
"allowMemberUnsubscribe": false,
"isPrivate": false
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create list"
}
```
The response includes `allowMemberUnsubscribe`. Updates that set the same value can be safely retried; creating a list again creates another list, so inspect your lists after an uncertain creation response.
A non-boolean or `null` value for `allowMemberUnsubscribe` returns HTTP 422 without applying the change. Existing authentication and list-write permissions still apply.
# Delete List
Source: https://docs.sequenzy.com/api-reference/lists/delete
DELETE /api/v1/lists/{listId}
Delete a subscriber list
Delete a subscriber list and remove all of its memberships. Subscribers on the list are not deleted, they only lose the list membership. This action cannot be undone.
## Request
Subscriber list ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/lists/list_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"removedMemberships": 1250
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "List not found"
}
```
# List Subscriber Lists
Source: https://docs.sequenzy.com/api-reference/lists/list
GET /api/v1/lists
List subscriber lists
List subscriber lists for the authenticated company. Each list includes `subscriberCount` (current members of any status) and `activeSubscriberCount` (current members with status=`active`). Members who unsubscribed from the list are not counted.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/lists" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"lists": [
{
"id": "list_abc123",
"name": "Newsletter Subscribers",
"description": "Subscribers interested in newsletter",
"isPrivate": false,
"allowMemberUnsubscribe": false,
"createdAt": "2026-05-01T10:30:00Z",
"subscriberCount": 1200,
"activeSubscriberCount": 1180
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
Each list includes `allowMemberUnsubscribe`, a boolean that defaults to `false`. When enabled on a private list, only its current members can see its name and opt out through preferences. It does not permit joining or rejoining, and has no effect on public lists.
# Remove Subscribers from List
Source: https://docs.sequenzy.com/api-reference/lists/remove-subscribers
POST /api/v1/lists/{listId}/subscribers/remove
Remove subscribers from a subscriber list
Remove subscribers from one list by email or subscriber ID. Subscribers are not deleted, they only lose the list membership. You can pass up to 500 targets per request, combined across `emails` and `subscriberIds`.
## Permissions
This endpoint needs both `lists:write` and `subscribers:write`, the same pair as
[Add Subscribers To List](/api-reference/lists/add-subscribers). Membership is
contact state, so `lists:write` on its own only lets a key create and rename
lists. The Safer agent access preset includes `lists:write` but not
`subscribers:write`, so keys on that preset cannot change who is in a list.
## Request
Subscriber list ID.
Email addresses to remove. Emails are trimmed and lowercased. Unknown emails
are returned in `notFound`.
Subscriber IDs to remove.
Provide at least one of `emails` or `subscriberIds`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/lists/list_abc123/subscribers/remove" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails": ["user@example.com", "unknown@example.com"]}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"removed": 1,
"notFound": ["unknown@example.com"]
}
```
```json 400 theme={null}
{
"error": "Provide emails or subscriberIds"
}
```
```json 400 theme={null}
{
"error": "A maximum of 500 subscribers can be removed per request"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "List not found"
}
```
# Update List
Source: https://docs.sequenzy.com/api-reference/lists/update
PATCH /api/v1/lists/{listId}
Update a subscriber list
Update a subscriber list's name, description, or privacy flag. Only the fields you provide are changed.
## Request
Subscriber list ID.
New list name.
New internal list description. It is never shown in hosted or embedded
subscriber preferences. Pass `null` to clear it.
Private lists stay hidden from subscriber preferences unless you enable member
unsubscribe. Public lists can be discovered and joined there. Descriptions
always stay internal. Omission defaults to public on creation and preserves
visibility on update.
Allow existing members of a private list to see its name and unsubscribe in
email preferences. Defaults to `false` on creation. On update, omit to
preserve the setting or pass `false` to disable it; `null` is rejected. The
setting is stored but has no effect on public lists. Members cannot join or
rejoin private lists through preferences.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/lists/list_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Newsletter Subscribers", "isPrivate": false}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"list": {
"id": "list_abc123",
"name": "Newsletter Subscribers",
"description": "Weekly newsletter audience",
"isPrivate": false,
"allowMemberUnsubscribe": false,
"createdAt": "2026-05-20T10:00:00.000Z"
}
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "List not found"
}
```
```json 500 theme={null}
{
"error": "Failed to update list"
}
```
The response includes `allowMemberUnsubscribe`. Updates that set the same value can be safely retried; creating a list again creates another list, so inspect your lists after an uncertain creation response.
A non-boolean or `null` value for `allowMemberUnsubscribe` returns HTTP 422 without applying the change. Existing authentication and list-write permissions still apply.
# Complete Image Upload
Source: https://docs.sequenzy.com/api-reference/media/complete-upload
POST /api/v1/media/complete-upload
Register an uploaded image in the shared media library
After uploading image bytes to the authenticated URL from [Create Image Upload
URL](/api-reference/media/upload-url), register the returned company-scoped
`key`. Completion is idempotent, so retrying the same key returns the existing
asset instead of creating a duplicate. The response contains the permanent
hosted URL to use as an image block's `src`.
## Request
Storage key returned by the upload-url endpoint. It must belong to the
authenticated company.
Hosted image file name.
Uploaded image MIME type: PNG, JPEG, GIF, or WebP.
Exact uploaded image size in bytes.
Optional intrinsic image width in pixels.
Optional intrinsic image height in pixels.
Accessible image description, or an empty string for a decorative image.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/media/complete-upload" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key":"email-images/comp_123/.../product-shot.png",
"filename":"product-shot.png",
"contentType":"image/png",
"fileSizeBytes":248132,
"width":1440,
"height":900,
"altText":"HeyStream product results screen"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"asset": {
"id": "media_123",
"filename": "product-shot.png",
"url": "https://images.sequenzy.com/email-images/comp_123/.../product-shot.png",
"mimeType": "image/png",
"size": "248132",
"width": "1440",
"height": "900",
"altText": "HeyStream product results screen",
"companyId": "comp_123"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "The uploaded image key is invalid for this company."
}
```
```json 409 theme={null}
{
"success": false,
"error": "The uploaded image changed during verification. Start a new upload."
}
```
# Upload Image Bytes
Source: https://docs.sequenzy.com/api-reference/media/upload-bytes
PUT /api/v1/media/upload-bytes
Validate and store image bytes at the authenticated upload URL
Upload the exact image bytes to the `uploadUrl` returned by [Create Image
Upload URL](/api-reference/media/upload-url). Send the same API key and company
selection headers used to create the URL. The query parameters are already
included in `uploadUrl`; do not modify them.
The server reads at most the declared size (up to 10MB), verifies the PNG, JPEG,
GIF, or WebP file signature, and only then creates the public object. Each URL
can create its object once, so request a new upload URL after a `409` response.
```bash theme={null}
curl -X PUT "UPLOAD_URL_FROM_RESPONSE" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: image/png" \
--data-binary @product-shot.png
```
## Responses
```json 200 theme={null}
{
"success": true,
"key": "email-images/comp_123/.../product-shot.png",
"publicUrl": "https://images.sequenzy.com/email-images/comp_123/.../product-shot.png"
}
```
```json 400 theme={null}
{
"success": false,
"error": "The uploaded image bytes do not match the declared content type."
}
```
```json 409 theme={null}
{
"success": false,
"error": "This image upload URL has already been used."
}
```
# Create Image Upload URL
Source: https://docs.sequenzy.com/api-reference/media/upload-url
POST /api/v1/media/upload-url
Get a presigned URL for a block-ready email image
Returns a company-scoped authenticated API URL for uploading an image to
Sequenzy's shared media library. PUT the exact bytes to `uploadUrl` with the
same API credentials, then call [Complete Image
Upload](/api-reference/media/complete-upload) with the returned `key`.
PNG, JPEG, GIF, and WebP images up to 10MB are accepted. These formats can be
used immediately in email image blocks without an asynchronous conversion.
## Request
Original file name, including a supported image extension.
Image MIME type, such as `image/png`.
Exact image size in bytes, from 1 through 10485760.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/media/upload-url" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"product-shot.png","contentType":"image/png","fileSizeBytes":248132}'
curl -X PUT "UPLOAD_URL_FROM_RESPONSE" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: image/png" \
--data-binary @product-shot.png
```
## Responses
```json 200 theme={null}
{
"success": true,
"uploadUrl": "https://api.sequenzy.com/api/v1/media/upload-bytes?...",
"publicUrl": "https://images.sequenzy.com/email-images/comp_123/.../product-shot.png",
"key": "email-images/comp_123/.../product-shot.png",
"fileName": "product-shot.png"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unsupported image type. Use PNG, JPEG, GIF, or WebP."
}
```
```json 503 theme={null}
{
"success": false,
"error": "Image uploads are not configured on this server."
}
```
# Get Migration Agent Package
Source: https://docs.sequenzy.com/api-reference/migrations/agent-package
GET /api/v1/migrations/{runId}/agent-package
Return the exact migration gateway endpoints and call sequence for an agent.
Returns provider, run ID, endpoint URLs, auth instructions, exact method
sequence, and migration-agent instructions.
Agents must preserve source email HTML exactly. Migrated campaigns, templates,
and sequence emails should be copied as raw HTML, not recreated or converted
into Sequenzy blocks. If exact provider HTML is unavailable, flag the item for
manual review.
## Request
Migration run ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/migrations/run_123/agent-package" \
-H "Authorization: Bearer SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"runId":"run_123","provider":"kit","systemPrompt":"Preserve provider email HTML exactly...","instructions":["Do not convert migrated HTML into Sequenzy blocks or recreate the email from its visible text."],"endpoints":{"connectSource":"https://api.sequenzy.com/api/v1/migrations/run_123/connect-source"},"sequence":["POST connect-source with the provider credential."]}}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 403 theme={null}
{"error":"Access denied to the migration run"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Approve Migration Plan
Source: https://docs.sequenzy.com/api-reference/migrations/approve-plan
POST /api/v1/migrations/{runId}/approve-plan
Approve provider-neutral migration resources for execution.
Approves resources from discovery and freezes the provider-neutral plan. Execution is still queued by `start`.
## Request
Migration run ID.
Discovery resource IDs to import.
Optional per-resource filters, such as automation names.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/migrations/run_123/approve-plan" \
-H "Authorization: Bearer SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"resourceIds":["subscribers","tags","custom_fields","automations"],"resourceOptions":{"automations":{"names":["Budget Worksheet","Mini Budget Binder Flow"]}}}'
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"awaiting_execution","approvedPlan":{"resources":[]}}}
```
```json 400 theme={null}
{"error":"Migration discovery is required before approval"}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 500 theme={null}
{"success":false,"error":"Failed to approve migration plan"}
```
# Cancel Migration
Source: https://docs.sequenzy.com/api-reference/migrations/cancel
POST /api/v1/migrations/{runId}/cancel
Cancel a migration run.
Cancels queued/pre-execution runs immediately. Running imports move to `cancel_requested` while workers stop linked subscriber import chunks, then finish as `canceled`.
## Request
Migration run ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/migrations/run_123/cancel" \
-H "Authorization: Bearer SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"canceled"}}
```
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"cancel_requested"}}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 403 theme={null}
{"error":"Access denied to the migration run"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Connect Migration Source
Source: https://docs.sequenzy.com/api-reference/migrations/connect-source
POST /api/v1/migrations/{runId}/connect-source
Attach a provider credential to an existing migration run.
Stores the source credential for the run connection. Discovery and imports are queued separately.
## Request
Migration run ID.
Provider adapter ID. Supported values include `active-campaign`, `brevo`, `constant-contact`, `customer-io`, `drip`, `hubspot`, `kit`, `klaviyo`, `loops`, `mailchimp`, `mailerlite`, `mailjet`, `omnisend`, `resend`, and `sendgrid`.
Provider API credential.
Optional display label for manual providers.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/migrations/run_123/connect-source" \
-H "Authorization: Bearer SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"mailchimp","credential":"PROVIDER_API_KEY"}'
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"pending","provider":"mailchimp"}}
```
```json 400 theme={null}
{"success":false,"error":"Migration provider is not available"}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Discover Migration Source
Source: https://docs.sequenzy.com/api-reference/migrations/discover
POST /api/v1/migrations/{runId}/discover
Queue source discovery for a migration run.
Queues provider discovery. The provider adapter returns provider-neutral resources.
## Request
Migration run ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/migrations/run_123/discover" \
-H "Authorization: Bearer SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"discovering"}}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 403 theme={null}
{"error":"Access denied to the migration run"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Get Migration Run
Source: https://docs.sequenzy.com/api-reference/migrations/get
GET /api/v1/migrations/{runId}
Read a provider-agnostic migration run.
Returns run status, discovery, approved plan, progress, report, and errors.
## Request
Migration run ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/migrations/run_123" \
-H "Authorization: Bearer SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"awaiting_approval","discovery":{"provider":"kit","resources":[{"id":"subscribers","type":"subscribers","count":3470},{"id":"campaigns","type":"campaigns","count":108},{"id":"automations","type":"automations","count":42}]},"approvedPlan":{},"progress":{},"report":{"subscribersQueued":0,"draftCampaignsCreated":0,"draftSequencesCreated":0,"draftTransactionalEmailsCreated":0}}}
```
```json 401 theme={null}
{"error":"Missing API key. Provide via x-api-key header or Authorization: Bearer "}
```
```json 403 theme={null}
{"error":"Access denied to the migration run"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Start Migration
Source: https://docs.sequenzy.com/api-reference/migrations/start
POST /api/v1/migrations/{runId}/start
Queue execution for an approved migration run.
Queues the generic migration worker. Optionally approves `resourceIds` first.
## State and retries
At least one resource must be selected, either in `resourceIds` or in the
previously approved plan. Providing `resourceIds` approves that selection before
starting. `resourceOptions` is used only with a nonempty `resourceIds` selection.
If the run is already `queued` or `running`, this endpoint returns its current
state without queueing another execution or changing its plan. Retry with the
same run ID after an uncertain response. Runs in `completed`, `failed`,
`canceled` or `cancel_requested` state cannot be started again and return `400`.
## Request
Migration run ID.
Optional resources to approve before start.
Optional per-resource filters when `resourceIds` is provided.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/migrations/run_123/start" \
-H "Authorization: Bearer SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"resourceIds":["subscribers","tags","custom_fields","automations"],"resourceOptions":{"automations":{"names":["Budget Worksheet"]}}}'
```
## Responses
```json 200 theme={null}
{"success":true,"data":{"id":"run_123","status":"queued"}}
```
```json 400 Invalid state theme={null}
{"success":false,"error":"This migration run cannot be started in its current state."}
```
```json 400 No selected resources theme={null}
{"success":false,"error":"Select at least one resource to migrate."}
```
```json 401 theme={null}
{"error":"Invalid API key"}
```
```json 403 theme={null}
{"error":"Access denied to the migration run"}
```
```json 404 theme={null}
{"error":"Migration run not found"}
```
# Get Notification Preferences
Source: https://docs.sequenzy.com/api-reference/notification-preferences/get
GET /api/v1/notification-preferences
Get your account notification settings for the current company
Get which account notifications Sequenzy emails you about this workspace: when a contact joins, when a form is submitted, when a campaign finishes sending, and the Monday weekly report.
Instant form-submission notifications stop after 50 per workspace per UTC day. Form submissions continue to be processed after that cap; only the account-notification email is suppressed.
These are per-person settings. This endpoint always acts on the API key's own user, and there is no way to read a teammate's preferences through the API.
This endpoint requires the `account:read` scope.
Requests whose `User-Agent` is Node's default (`node` or `undici`) and that
send no `x-sequenzy-client` header receive only the three original events,
without `weekly_report`, because Sequenzy CLI releases before 0.0.124 crash on
events they do not know. Set any `User-Agent` or `x-sequenzy-client:
your-app/1.0` to get the full list. This shim will be removed once those CLI
versions are retired.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/notification-preferences" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Every notification event with its current mode. Events you have never
configured report the platform default rather than being omitted, so the array
always has one entry per event.
`new_subscriber`, `form_submitted`, `campaign_completed`, or `weekly_report`.
`off`, `instant` (one email per occurrence), `daily` (one summary per day), or
`weekly` (one report per week).
Modes each event accepts, keyed by event. `form_submitted` and
`campaign_completed` do not accept `daily`; `weekly_report` accepts only `off`
and `weekly`.
Mode each event uses when you have never configured it. Resolved for your
role: the workspace owner gets `weekly_report` as `weekly`, invited members
get `off` until they opt in.
## Responses
```json 200 theme={null}
{
"success": true,
"notificationPreferences": [
{ "event": "new_subscriber", "mode": "instant" },
{ "event": "form_submitted", "mode": "instant" },
{ "event": "campaign_completed", "mode": "instant" },
{ "event": "weekly_report", "mode": "weekly" }
],
"supportedModes": {
"new_subscriber": ["off", "instant", "daily"],
"form_submitted": ["off", "instant"],
"campaign_completed": ["off", "instant"],
"weekly_report": ["off", "weekly"]
},
"defaults": {
"new_subscriber": "off",
"form_submitted": "off",
"campaign_completed": "off",
"weekly_report": "weekly"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": { "code": "UNAUTHORIZED", "message": "Invalid API key" }
}
```
See [Account Notifications](/concepts/account-notifications) for when each notification fires and how the daily cap works.
# Update Notification Preferences
Source: https://docs.sequenzy.com/api-reference/notification-preferences/update
PATCH /api/v1/notification-preferences
Change your account notification settings for the current company
Change which account notifications Sequenzy emails you about this workspace.
This endpoint requires the `companies:manage` scope. Reading the current values
only requires `account:read`.
Events you do not include keep their current value. The response returns the full set either way, so you never have to merge state yourself.
This endpoint always acts on the API key's own user and can never change a teammate's settings.
Instant form-submission notifications stop after 50 per workspace per UTC day. Form submissions continue to be processed after that cap; only the account-notification email is suppressed.
Requests whose `User-Agent` is Node's default (`node` or `undici`) and that
send no `x-sequenzy-client` header receive only the three original events,
without `weekly_report`, because Sequenzy CLI releases before 0.0.124 crash on
events they do not know. Set any `User-Agent` or `x-sequenzy-client:
your-app/1.0` to get the full list. This shim will be removed once those CLI
versions are retired.
## Request
Preferences to set. Must contain at least one entry.
`new_subscriber`, `form_submitted`, `campaign_completed`, or `weekly_report`.
`off`, `instant`, `daily`, or `weekly`. `daily` is not valid for
`form_submitted` or `campaign_completed` - each submission and each campaign
finish is its own event, so there is nothing to roll up. `weekly_report`
accepts only `off` and `weekly`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/notification-preferences" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"notificationPreferences": [{"event": "new_subscriber", "mode": "daily"}]}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"notificationPreferences": [
{ "event": "new_subscriber", "mode": "daily" },
{ "event": "form_submitted", "mode": "off" },
{ "event": "campaign_completed", "mode": "instant" },
{ "event": "weekly_report", "mode": "weekly" }
],
"supportedModes": {
"new_subscriber": ["off", "instant", "daily"],
"form_submitted": ["off", "instant"],
"campaign_completed": ["off", "instant"],
"weekly_report": ["off", "weekly"]
},
"defaults": {
"new_subscriber": "off",
"form_submitted": "off",
"campaign_completed": "off",
"weekly_report": "weekly"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "\"daily\" is not a supported mode for the campaign_completed notification."
}
}
```
```json 401 theme={null}
{
"success": false,
"error": { "code": "UNAUTHORIZED", "message": "Invalid API key" }
}
```
## Before a bulk import
You do not need to turn notifications off before importing contacts. Imports never trigger new-subscriber notifications, and a high-volume day of real signups automatically falls back to a daily summary. See [Account Notifications](/concepts/account-notifications).
# Track Checkout Started
Source: https://docs.sequenzy.com/api-reference/orders/checkout-started
POST /api/v1/checkouts
Track a started checkout for abandoned checkout automations
Track a started checkout from any e-commerce platform. Triggers the `ecommerce.checkout_started` event, which can power abandoned checkout automations (start a sequence on `ecommerce.checkout_started` with a delay, and stop it on `ecommerce.order_placed`).
## Request Body
Unique checkout identifier in your platform.
Checkout total in cents.
ISO 4217 currency code (e.g. `USD`).
URL the customer can use to resume the checkout. Available in emails as
`{{event.checkoutUrl}}`.
The customer who started the checkout. Requires `email`; also accepts
`externalId`, `firstName`, `lastName`, and `attributes`.
Checkout line items. Each item has `productId` (required), `title`
(required), `quantity` (required), `variantId`, `sku`, `variantTitle`, and
`priceCents`.
Extra event properties to attach to the triggered event.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/checkouts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"checkoutId": "checkout-abc123",
"totalCents": 8850,
"currency": "USD",
"checkoutUrl": "https://store.example.com/checkout/abc123",
"customer": { "email": "buyer@example.com" },
"items": [
{
"productId": "SKU-PROTEIN-1KG",
"title": "Protein Powder",
"quantity": 1,
"priceCents": 8850
}
]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"checkoutId": "checkout-abc123",
"subscriberId": "sub_abc123",
"eventId": "evt_xyz789"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 422 theme={null}
{
"success": false,
"error": "Validation error: checkoutId is required"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Push Order
Source: https://docs.sequenzy.com/api-reference/orders/create
POST /api/v1/orders
Push a normalized order from any e-commerce platform
Push an order from any e-commerce platform. This is the core of the [Commerce API](/concepts/custom-commerce): one call gives you the same behavior as a native Shopify/WooCommerce order webhook.
When an order is pushed, Sequenzy:
1. **Upserts the customer** as a subscriber (with names and custom attributes)
2. **Triggers the matching event** (`ecommerce.order_placed`, `ecommerce.order_cancelled`, `ecommerce.order_fulfilled`, or `ecommerce.order_refunded`) for automations and segments
3. **Updates revenue attributes** on the subscriber: `ltv`, `totalSpent`, `ordersCount`, `aov` (placed orders only)
4. **Cancels superseded automations** (e.g. a replenishment or back-in-stock sequence for a product the customer just bought)
5. **Schedules replenishment reminders** for products with replenishment enabled
Processing is asynchronous - the endpoint returns `202 Accepted` immediately.
## Idempotency
Pushing the same `orderId` twice never double counts revenue. Retries are safe.
## Request Body
Unique order identifier in your platform. Used for idempotency.
Human-facing order number, if different from `orderId`.
Lifecycle status of this order event: `placed`, `cancelled`, `fulfilled`, or
`refunded`.
Order total in cents.
ISO 4217 currency code (e.g. `USD`).
ISO 8601 timestamp of when the order happened. Defaults to now.
The customer who placed the order. Requires `email`; also accepts
`externalId`, `firstName`, `lastName`, and `attributes` (custom subscriber
attributes, synced for segment filtering).
Order line items. Each item has `productId` (required), `title` (required),
`quantity` (required), `variantId`, `sku`, `variantTitle`, and `priceCents`.
Use the same `productId` values as your [product
catalog](/api-reference/products/upsert) so replenishment and product matching
work. Line items do not carry `imageUrl` or `url`: Sequenzy fills those in
from the matching catalog product when it renders an email, so a `productId`
with no catalog match still processes the order but produces line items with
no image or link. See [what lands in the email](/concepts/custom-commerce).
For `refunded` orders: refunded amount in cents.
Authoritative customer aggregates from your platform: `ordersCount` and
`totalSpentCents`. When provided, these override Sequenzy's additive revenue
bookkeeping (recommended if your platform tracks lifetime totals).
Extra event properties to attach to the triggered `ecommerce.*` event.
Available in sequence emails as `{{event.*}}` merge tags. Reserved
normalized keys (`provider`, `orderId`, `lineItems`, `orderedAt`, etc.)
cannot be overridden.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "order-1001",
"totalCents": 8850,
"currency": "USD",
"orderedAt": "2026-06-01T12:00:00.000Z",
"customer": {
"email": "buyer@example.com",
"firstName": "Jane",
"attributes": { "acquisitionChannel": "tiktok" }
},
"items": [
{
"productId": "SKU-PROTEIN-1KG",
"variantId": "SKU-PROTEIN-1KG-VANILLA",
"title": "Protein Powder",
"variantTitle": "Vanilla",
"quantity": 2,
"priceCents": 4425
}
]
}'
```
### Refund example
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "order-1001",
"status": "refunded",
"totalCents": 8850,
"refundAmountCents": 8850,
"currency": "USD",
"customer": { "email": "buyer@example.com" }
}'
```
## Responses
```json 202 theme={null}
{
"success": true,
"queued": true,
"jobId": "commerce-order-1a2b3c",
"orderId": "order-1001",
"status": "placed"
}
```
```json 400 theme={null}
{
"success": false,
"error": "orderedAt must be a valid ISO 8601 timestamp"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 422 theme={null}
{
"success": false,
"error": "Validation error: customer.email is required"
}
```
```json 503 theme={null}
{
"success": false,
"error": "Failed to queue order"
}
```
# Attach Delivery File
Source: https://docs.sequenzy.com/api-reference/products/attach-delivery
PUT /api/v1/products/{productId}/delivery
Attach the distributable file delivered after a purchase
Attach the distributable file buyers receive after purchasing this product. Purchase events then expose it as `{{event.download.url}}` and `{{event.download.name}}` in sequence emails.
## Request
Product ID (from [List Products](/api-reference/products/list)). For products
pushed via the Commerce API, you can also use your own productId (e.g.
`my-ebook`).
Public http(s) URL of the file. Use [Create Upload
URL](/api-reference/products/upload-url) to host the file on Sequenzy, or link
to a file hosted elsewhere.
`upload` for files hosted on Sequenzy, `url` for external links.
Display name for the file (e.g. `guide.pdf`). Used as `{{event.download.name}}`.
File size in bytes.
File MIME type.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/products/prod_internal_1/delivery" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/guide.pdf", "fileName": "guide.pdf"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"product": {
"id": "prod_internal_1",
"title": "The Indie Hacker Guide",
"digitalDelivery": {
"url": "https://example.com/guide.pdf",
"source": "url",
"fileName": "guide.pdf",
"fileSizeBytes": null,
"mimeType": null,
"updatedAt": "2026-06-11T00:00:00.000Z"
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Delivery URL must start with http:// or https://"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Product not found"
}
```
# Register Back-in-Stock Request
Source: https://docs.sequenzy.com/api-reference/products/back-in-stock
POST /api/v1/back-in-stock
Register a customer's back-in-stock notification request
Register a customer's request to be notified when a product is back in stock. When a later [product upsert](/api-reference/products/upsert) marks the product or variant in stock again, the `ecommerce.back_in_stock` event fires for waiting subscribers, which can trigger automations.
This is the Commerce API equivalent of the Shopify/WooCommerce storefront back-in-stock widgets. Call it from your own storefront or backend when a customer asks to be notified.
## Request Body
Your product identifier (the `productId` used when upserting products).
Your variant identifier. Defaults to `productId` for products without
variants.
Product title snapshot used in notifications. Defaults to the synced product
title.
Variant title snapshot used in notifications. Defaults to the synced variant
title.
The customer requesting the notification. Requires `email`; also accepts
`externalId`, `firstName`, `lastName`, and `attributes`. The subscriber is
created if they don't exist.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/back-in-stock" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"productId": "SKU-PROTEIN-1KG",
"variantId": "SKU-PROTEIN-1KG-VANILLA",
"customer": { "email": "buyer@example.com" }
}'
```
## What Happens Next
1. The subscriber is created (if needed) without triggering signup automations
2. The `ecommerce.back_in_stock_requested` event is recorded
3. When a product upsert marks the variant in stock again, the
`ecommerce.back_in_stock` event fires and can start automations
## Responses
```json 200 theme={null}
{
"success": true,
"subscriberId": "sub_abc123"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Failed to create subscriber"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Delete Product
Source: https://docs.sequenzy.com/api-reference/products/delete
DELETE /api/v1/products/{productId}
Delete a product pushed via the Commerce API
Delete a product previously pushed via the [Commerce API](/concepts/custom-commerce), identified by your `productId`. Products synced from Shopify/WooCommerce are not affected by this endpoint.
## Path Parameters
Your product identifier (the `productId` used when upserting).
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/products/SKU-PROTEIN-1KG" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"deleted": true
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Product not found"
}
```
# Get Product
Source: https://docs.sequenzy.com/api-reference/products/get
GET /api/v1/products/{productId}
Get a product from your catalog
Get a product from your catalog. This includes products pushed via the [Commerce API](/concepts/custom-commerce), synced from integrations, and Stripe products with attached digital delivery files.
## Path Parameters
Your product identifier (the `productId` used when upserting) or the internal product ID returned by [List Products](/api-reference/products/list).
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/products/SKU-PROTEIN-1KG" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"product": {
"id": "prod_abc123",
"productId": "SKU-PROTEIN-1KG",
"provider": "api",
"title": "Protein Powder",
"priceCents": 8850,
"currency": "USD",
"inStock": true,
"variants": [],
"digitalDelivery": null
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Product not found"
}
```
# List Products
Source: https://docs.sequenzy.com/api-reference/products/list
GET /api/v1/products
List products in your catalog
List products in your catalog. Includes products synced from Shopify, WooCommerce, and Stripe, plus products pushed via the [Commerce API](/concepts/custom-commerce). Stripe products include attached digital delivery files when configured.
For Stripe products, `variants` contains every active Stripe price of the product (`variantId` is the Stripe price ID), so you can target an exact price even when it is not the product's default. The top-level `priceCents` follows the product's default price while it stays active, and falls back to the newest active price when the default has been archived.
## Query Parameters
Case-insensitive substring search on product title. Combines with the
`provider` filter and pagination.
Maximum number of products to return (1-100). Defaults to 50.
Number of products to skip. Defaults to 0.
Filter products by source: `api`, `stripe`, `shopify`, `woocommerce`, or
`manual`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/products?provider=stripe&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Pagination
The response returns one page. `pagination.count` is the size of that page, while `pagination.total` is the number of products matching your filters across the whole catalog. When `pagination.hasMore` is `true`, request the next page by adding `limit` to `offset`:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/products?limit=100&offset=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"products": [
{
"id": "prod_abc123",
"productId": "SKU-PROTEIN-1KG",
"provider": "api",
"title": "Protein Powder",
"priceCents": 8850,
"currency": "USD",
"inStock": true,
"variants": [],
"digitalDelivery": null
}
],
"pagination": {
"limit": 50,
"offset": 0,
"count": 1,
"total": 1,
"hasMore": false
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid provider. Expected one of: api, stripe, shopify, woocommerce, manual"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
# Remove Delivery File
Source: https://docs.sequenzy.com/api-reference/products/remove-delivery
DELETE /api/v1/products/{productId}/delivery
Remove the attached distributable file from a product
Remove the attached distributable file from a product. Future purchases of this product will no longer include `download` properties on the purchase event.
## Request
Product ID (from [List Products](/api-reference/products/list)). For products
pushed via the Commerce API, you can also use your own productId (e.g.
`my-ebook`).
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/products/prod_internal_1/delivery" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"product": {
"id": "prod_internal_1",
"title": "The Indie Hacker Guide",
"digitalDelivery": null
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Product not found"
}
```
# Sync Stripe Products
Source: https://docs.sequenzy.com/api-reference/products/sync
POST /api/v1/products/sync
Queue a sync of the Stripe product catalog
Queues a background sync of the Stripe product catalog into the products list. Requires an active Stripe integration with bulk sync enabled. When multiple Stripe accounts are connected, pass `integrationId`; otherwise Sequenzy uses the most recently connected enabled account.
## Request
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/products/sync" \
-H "Authorization: Bearer YOUR_API_KEY"
```
To select a specific Stripe account:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/products/sync?integrationId=int_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"jobId": "12345"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 404 theme={null}
{
"success": false,
"error": "No active Stripe integration found. Connect Stripe in the dashboard first."
}
```
```json 409 theme={null}
{
"success": false,
"error": "Bulk sync is disabled for every active Stripe integration."
}
```
# Create Upload URL
Source: https://docs.sequenzy.com/api-reference/products/upload-url
POST /api/v1/products/delivery/upload-url
Get a presigned URL to upload a distributable file
Returns a presigned URL to upload a distributable file to Sequenzy storage. PUT the file bytes to `uploadUrl`, then attach `publicUrl` to a product with [Attach Delivery File](/api-reference/products/attach-delivery).
Allowed types: PDF, ePub, ZIP, images, audio, video, and text files, up to 100MB. Files are stored under unguessable URLs.
## Request
Original file name (used to build the hosted file name).
File MIME type, e.g. `application/pdf`.
File size in bytes (max 104857600).
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/products/delivery/upload-url" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "guide.pdf", "contentType": "application/pdf", "fileSizeBytes": 1048576}'
# Then upload the file bytes:
curl -X PUT "UPLOAD_URL_FROM_RESPONSE" \
-H "Content-Type: application/pdf" \
--data-binary @guide.pdf
```
## Responses
```json 200 theme={null}
{
"success": true,
"uploadUrl": "https://...r2.cloudflarestorage.com/...",
"publicUrl": "https://images.sequenzy.com/product-files/comp_123/9f2.../guide.pdf",
"fileName": "guide.pdf"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unsupported file type. Use PDF, ePub, ZIP, image, audio, video, or text files."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 503 theme={null}
{
"success": false,
"error": "File uploads are not configured on this server. Attach an external URL instead."
}
```
# Upsert Products
Source: https://docs.sequenzy.com/api-reference/products/upsert
POST /api/v1/products
Create or update products from any e-commerce platform
Create or update up to 100 products in your Sequenzy catalog, keyed by your `productId`. Products pushed here behave exactly like products synced from Shopify or WooCommerce - they power product blocks in emails, replenishment reminders, back-in-stock notifications, and segmentation.
Use this endpoint to connect any e-commerce platform (custom checkout, CheckoutChamp, Sticky.io, your own backend) to Sequenzy. See the [custom commerce integration guide](/concepts/custom-commerce) for the full walkthrough.
Updates are partial: optional fields you omit keep their stored values, so a stock-only update like `{"productId": "SKU-1", "title": "...", "inStock": false}` does not erase the product's image, URL, or pricing. Pass an explicit `null` to clear a stored value.
## Request Body
Products to create or update (1-100 per request). Each product is upserted by
its `productId`.
Your product identifier. Used as the upsert key - pushing the same `productId`
again updates the product.
Product title.
Product description.
Product image URL.
Public product page URL. Used for links in emails.
Price in cents. Defaults to the lowest variant price when variants are
provided.
Compare-at (strikethrough) price in cents.
ISO 4217 currency code (e.g. `USD`).
Whether the product is in stock. Defaults to `true`, or to whether any variant
is available when variants are provided.
ISO 8601 date-time when the product was created in your source platform. Omit
to preserve the stored timestamp; send `null` to clear it.
Product variants. Each variant has `variantId` (required), `title` (required),
`sku`, `priceCents`, `compareAtPriceCents`, `imageUrl`, `inStock`,
`inventoryQuantity`, and `options` (array of objects with `name` and `value`
strings). When provided, the variant list is replaced entirely - pass an empty
array to remove all variants. Omit the field to leave existing variants
unchanged.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/products" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"productId": "SKU-PROTEIN-1KG",
"title": "Protein Powder",
"description": "Whey protein, 1kg bag",
"imageUrl": "https://cdn.example.com/protein.jpg",
"url": "https://store.example.com/products/protein",
"currency": "USD",
"variants": [
{
"variantId": "SKU-PROTEIN-1KG-VANILLA",
"title": "Vanilla",
"priceCents": 8850,
"inventoryQuantity": 12,
"options": [{ "name": "Flavor", "value": "Vanilla" }]
},
{
"variantId": "SKU-PROTEIN-1KG-CHOC",
"title": "Chocolate",
"priceCents": 8850,
"inStock": false,
"options": [{ "name": "Flavor", "value": "Chocolate" }]
}
]
}
]
}'
```
## Stock Transitions
When an upsert changes a product or variant from out of stock to in stock, Sequenzy automatically fires the `ecommerce.back_in_stock` event for subscribers who [requested a back-in-stock notification](/api-reference/products/back-in-stock). Going out of stock cancels pending notifications.
Replenishment settings configured in the dashboard are preserved across upserts.
## Responses
```json 200 theme={null}
{
"success": true,
"upserted": 1,
"backInStockEventsTriggered": 0,
"products": [
{
"id": "prod_abc123",
"productId": "SKU-PROTEIN-1KG",
"provider": "api",
"title": "Protein Powder",
"priceCents": 8850,
"currency": "USD",
"inStock": true,
"variants": [
{
"variantId": "SKU-PROTEIN-1KG-VANILLA",
"title": "Vanilla",
"priceCents": 8850,
"inStock": true,
"inventoryQuantity": 12
}
]
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 422 theme={null}
{
"success": false,
"error": "Validation error: products is required"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Save Firebase Credentials
Source: https://docs.sequenzy.com/api-reference/push/android-credentials
PUT /api/v1/push/settings/android
Connect Android push with a Firebase service account
Saves the Firebase service account used for Android push. The file must be a service account key with a private key, and `token_uri`, when present, must be Google's. It is encrypted at rest and never returned. The account needs the Firebase Cloud Messaging API Admin role.
`DELETE /api/v1/push/settings/android` removes the credentials. Android devices stop receiving push until you upload a key again; registered devices are kept.
Both require the `integrations:manage` scope and edit access to the workspace.
## Request
The service account JSON file from Firebase project settings > Service
accounts, as a JSON object or a string.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/push/settings/android" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data "{\"serviceAccount\": $(cat firebase-key.json)}"
```
## Responses
```json 200 theme={null}
{
"success": true,
"push": {
"web": {
"enabled": true,
"vapidPublicKey": "BH3tXU0JSzok9ND7DyeQRcEy..."
},
"ios": {
"configured": true,
"keyId": "ABC123DEFG",
"teamId": "DEF123GHIJ",
"bundleId": "com.example.app",
"environment": "production",
"lastError": null,
"lastErrorAt": null
},
"android": {
"configured": true,
"projectId": "demo-project",
"clientEmail": "push@demo-project.iam.gserviceaccount.com",
"lastError": null,
"lastErrorAt": null
},
"defaultIconUrl": "https://cdn.example.com/icon-192.png"
},
"message": "Firebase credentials saved."
}
```
```json 400 theme={null}
{
"error": "token_uri must be https://oauth2.googleapis.com/token."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
# Get Push Campaign Stats
Source: https://docs.sequenzy.com/api-reference/push/campaign-stats
GET /api/v1/push/campaigns/{campaignId}/stats
Read delivery and engagement stats for a push campaign
Returns delivery and engagement stats for a push campaign. Test sends are excluded.
| Field | Meaning |
| - | - |
| `sent` | Contacts whose notification at least one push service accepted |
| `delivered` | Contacts whose device reported displaying it (browsers report automatically; apps report through the events endpoint) |
| `clicked` | Contacts who opened it |
| `failed` | Contacts whose every device rejected it, or whose credentials were refused |
| `skipped` | Contacts with no active device on a set-up platform, or stopped by a cancel |
| `devicesTargeted` / `devicesAccepted` | Device-level counts |
Requires the `campaigns:read` scope.
## Request
The push campaign ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123/stats" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaignId": "cmp_abc123",
"status": "sent",
"stats": {
"total": 340,
"pending": 0,
"sent": 331,
"failed": 4,
"skipped": 5,
"delivered": 210,
"clicked": 48,
"devicesTargeted": 402,
"devicesAccepted": 389,
"skipReasons": [
{
"reason": "no_active_device",
"count": 5
}
],
"failureReasons": [
{
"reason": "all_devices_invalid",
"count": 4
}
]
}
}
```
```json 400 theme={null}
{
"error": "This campaign is not a push campaign."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:read"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
# Stop a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/cancel-campaign
POST /api/v1/push/campaigns/{campaignId}/cancel
Stop a scheduled or sending push campaign
Stops a scheduled or sending push campaign. Contacts who have not been sent the push yet are skipped with the reason `campaign_cancelled`; delivered notifications cannot be recalled. `POST /api/v1/campaigns/{campaignId}/cancel` does the same for push campaigns. Requires the `campaigns:send` scope.
## Request
The push campaign ID.
No request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123/cancel" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "cancelled",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "This campaign is not a push campaign."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:send"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
```json 409 theme={null}
{
"error": "Only scheduled or sending push campaigns can be stopped."
}
```
# Create a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/create-campaign
POST /api/v1/push/campaigns
Create a push campaign draft
Creates a push campaign draft. You can set content and audience now or later with [Update a push campaign](./update-campaign), then [send it](./send-campaign). Requires the `campaigns:write` scope.
## Request
Campaign name.
Audience, same shape as campaign `targetLists`: `{"type":"all"}`, `{"type":"lists","listIds":[...]}`, `{"type":"segment","segmentId":"..."}`, or `{"type":"rules","include":[...]}`. Only contacts with an active push device receive the campaign.
Notification title, up to 120 characters. Merge tags such as `{{FIRST_NAME|there}}` work.
Notification message, up to 500 characters. Merge tags work.
Link opened on tap: an https URL, an app deep link (`myapp://...`), or a merge
tag. Browsers only open https links. `null` clears it.
Optional https image shown in the notification.
Optional https icon for web push. Defaults to the icon in push settings.
Limit delivery to `web`, `ios`, and/or `android`. Omit or pass `[]` for every
platform.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"name":"Flash sale","title":"24h sale","body":"30% off everything","url":"https://shop.example.com/sale","targetLists":{"type":"all"}}'
```
## Responses
```json 201 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "draft",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "url: Use an https:// URL, an app deep link (myapp://...), or a merge tag"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:write"
}
```
# Unsubscribe a Push Device
Source: https://docs.sequenzy.com/api-reference/push/delete-device
DELETE /api/v1/push/devices/{deviceId}
Stop sending push to a device
Marks a device `unsubscribed` so it stops receiving push, for example when a person signs out of your app. Requires the `subscribers:write` scope.
This does not revoke the browser or app permission. A browser that still allows notifications registers again, unlinked from the contact, the next time the web SDK syncs; your app registers again whenever it calls [Register a push device](./register-device). To stop a browser for good, call `sequenzy.unsubscribeFromPush()` on your site or have the visitor turn off notifications.
## Request
The device ID from [List push devices](./list-devices) or [Register a push
device](./register-device).
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/push/devices/dev_123" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"deviceId": "dev_123",
"message": "Device unsubscribed. It stops receiving push until your site or app registers it again."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:write"
}
```
```json 404 theme={null}
{
"error": "Device not found"
}
```
# Duplicate a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/duplicate-campaign
POST /api/v1/push/campaigns/{campaignId}/duplicate
Copy a push campaign into a new draft
Copies a push campaign's content and audience into a new draft. Requires the `campaigns:write` scope.
## Request
The push campaign ID.
Name for the copy. Defaults to the original name followed by `(Copy)`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123/duplicate" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"name":"Flash sale (resend)"}'
```
## Responses
```json 201 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_def456",
"name": "Flash sale (resend)",
"type": "push",
"status": "draft",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "This campaign is not a push campaign."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:write"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
# Estimate Push Campaign Recipients
Source: https://docs.sequenzy.com/api-reference/push/estimate-campaign
POST /api/v1/push/campaigns/estimate
Count audience contacts with an active device
Counts the contacts in an audience (`audienceCount`) and those with an active device on the selected platforms (`eligibleCount`). Email status does not affect push eligibility. Requires the `campaigns:read` scope.
## Request
Audience, same shape as campaign `targetLists`.
Only count devices on these platforms.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns/estimate" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"targetLists":{"type":"all"}}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"audienceCount": 1200,
"eligibleCount": 340
}
```
```json 400 theme={null}
{
"error": "Invalid audience."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:read"
}
```
# Get a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/get-campaign
GET /api/v1/push/campaigns/{campaignId}
Read a push campaign and its stats
Returns a push campaign's content, audience and status. Once the campaign has started sending, `stats` holds its delivery and engagement counts; it is `null` for drafts. Requires the `campaigns:read` scope.
## Request
The push campaign ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "sent",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": "2026-10-01T15:02:11.000Z",
"estimatedRecipientCount": 340,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
},
"stats": {
"total": 340,
"pending": 0,
"sent": 331,
"failed": 4,
"skipped": 5,
"delivered": 210,
"clicked": 48,
"devicesTargeted": 402,
"devicesAccepted": 389,
"skipReasons": [
{
"reason": "no_active_device",
"count": 5
}
],
"failureReasons": [
{
"reason": "all_devices_invalid",
"count": 4
}
]
}
}
```
```json 400 theme={null}
{
"error": "This campaign is not a push campaign."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:read"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
# Save APNs Credentials
Source: https://docs.sequenzy.com/api-reference/push/ios-credentials
PUT /api/v1/push/settings/ios
Connect iOS push with an APNs auth key
Saves the APNs auth key (.p8) used for iOS push. The key is validated, encrypted at rest and never returned. Saving new credentials clears the last credential error.
`DELETE /api/v1/push/settings/ios` removes the credentials. iOS devices stop receiving push until you save a key again; registered devices are kept.
Both require the `integrations:manage` scope and edit access to the workspace.
## Request
The 10-character APNs key ID.
Your 10-character Apple developer team ID.
Your app's bundle ID, for example `com.example.app`.
The full contents of the `AuthKey_XXXXXXXXXX.p8` file.
`production` for App Store and TestFlight builds, `sandbox` for Xcode
development builds.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/push/settings/ios" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"keyId":"ABC123DEFG","teamId":"DEF123GHIJ","bundleId":"com.example.app","privateKey":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"push": {
"web": {
"enabled": true,
"vapidPublicKey": "BH3tXU0JSzok9ND7DyeQRcEy..."
},
"ios": {
"configured": true,
"keyId": "ABC123DEFG",
"teamId": "DEF123GHIJ",
"bundleId": "com.example.app",
"environment": "production",
"lastError": null,
"lastErrorAt": null
},
"android": {
"configured": false,
"projectId": null,
"clientEmail": null,
"lastError": null,
"lastErrorAt": null
},
"defaultIconUrl": "https://cdn.example.com/icon-192.png"
},
"message": "APNs credentials saved."
}
```
```json 400 theme={null}
{
"error": "The key could not be read. Paste the full contents of the .p8 file."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
# List Push Campaigns
Source: https://docs.sequenzy.com/api-reference/push/list-campaigns
GET /api/v1/push/campaigns
List push campaigns
Lists push campaigns, newest first. Push campaigns also appear in [List campaigns](../campaigns/list) with `type: "push"`. Requires the `campaigns:read` scope.
## Request
`draft`, `scheduled`, `sending`, `sent`, or `cancelled`.
Page size, 1 to 100.
Number of campaigns to skip.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/push/campaigns?status=sent" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaigns": [
{
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "draft",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
],
"total": 1
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:read"
}
```
# List Push Devices
Source: https://docs.sequenzy.com/api-reference/push/list-devices
GET /api/v1/push/devices
List registered browsers and app installs
Lists registered push devices in a stable order; use `nextCursor` to page. Tokens are never returned in full; `tokenPreview` shows the last characters. Requires the `subscribers:read` scope.
## Request
Only this contact's devices.
Only this subscriber's devices.
`web`, `ios`, or `android`.
`active`, `invalid` (rejected by the push service), or `unsubscribed`.
Page size, 1 to 100.
`nextCursor` from the previous page.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/push/devices?email=jane@example.com" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"devices": [
{
"id": "dev_123",
"subscriberId": "sub_456",
"platform": "ios",
"status": "active",
"invalidReason": null,
"source": "api",
"deviceName": "Jane iPhone",
"userAgent": null,
"appVersion": "3.2.0",
"osVersion": "18.1",
"locale": "en-US",
"timezone": "America/New_York",
"tokenPreview": "…9f3a21bc",
"lastSeenAt": "2026-10-01T12:00:00.000Z",
"createdAt": "2026-09-20T09:30:00.000Z"
}
],
"nextCursor": null
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 404 theme={null}
{
"error": "Subscriber not found."
}
```
# Register a Push Device
Source: https://docs.sequenzy.com/api-reference/push/register-device
POST /api/v1/push/devices
Register an APNs, FCM or web push token for a contact
Registers or refreshes a device, typically from your app backend after the app receives its APNs or Firebase token. Registering a known token again reactivates it, refreshes its metadata, and moves it to the contact you pass. An unknown email becomes a contact quietly: no lists and no `contact_added` sequences.
Each contact keeps up to 20 active devices; the least recently seen are retired. Returns `201` for a new token and `200` for a known one. Requires the `subscribers:write` scope.
## Request
`ios`, `android`, or `web`.
iOS: the hex APNs device token. Android: the FCM registration token. Web: the
`PushSubscription` endpoint (also send `keys`).
Web only: `PushSubscription.toJSON().keys` with `p256dh` and `auth`.
Contact to link. Created quietly when new.
Existing subscriber to link instead of `email`.
Label shown in the dashboard.
Your app version.
Operating system version.
Device locale, for example `en-US`.
IANA timezone.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/devices" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"platform":"ios","token":"8f2c9e...","email":"jane@example.com","deviceName":"Jane iPhone","appVersion":"3.2.0"}'
```
## Responses
```json 201 theme={null}
{
"success": true,
"created": true,
"device": {
"id": "dev_123",
"subscriberId": "sub_456",
"platform": "ios",
"status": "active",
"invalidReason": null,
"source": "api",
"deviceName": "Jane iPhone",
"userAgent": null,
"appVersion": "3.2.0",
"osVersion": "18.1",
"locale": "en-US",
"timezone": "America/New_York",
"tokenPreview": "…9f3a21bc",
"lastSeenAt": "2026-10-01T12:00:00.000Z",
"createdAt": "2026-09-20T09:30:00.000Z"
},
"message": "Device registered."
}
```
```json 400 theme={null}
{
"error": "iOS tokens are the hex APNs device token (64+ hex characters)."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:write"
}
```
```json 404 theme={null}
{
"error": "Subscriber not found."
}
```
# Send a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/send-campaign
POST /api/v1/push/campaigns/{campaignId}/send
Send a push campaign now or schedule it
Sends a draft or scheduled push campaign now, or schedules it when `scheduledAt` is a future ISO 8601 time. The campaign needs a title or message, an audience with at least one contact that has an active device, and at least one selected platform set up in push settings. The audience is re-evaluated when delivery starts.
Each contact receives one notification per active device. If a push service is temporarily unavailable, only devices that have not accepted the message are retried. Requires the `campaigns:send` scope.
## Request
The push campaign ID.
Future ISO 8601 time. Omit or pass `null` to send immediately.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123/send" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "sending",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": 340,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
},
"message": "Push campaign is sending to about 340 contacts."
}
```
```json 400 theme={null}
{
"error": "No selected contacts can receive push notifications. Contacts need at least one active device."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:send"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
```json 409 theme={null}
{
"error": "Only draft or scheduled push campaigns can be sent or rescheduled."
}
```
# Get Push Settings
Source: https://docs.sequenzy.com/api-reference/push/settings
GET /api/v1/push/settings
Read web push, APNs and Firebase readiness and device counts
Returns whether web push is on (with the VAPID public key browsers subscribe with), whether APNs (iOS) and Firebase (Android) credentials are connected, the last credential error for each platform, the default icon, the brand icon used when no default icon is set, and active device counts. Secrets are never returned. Requires the `account:read` scope.
See [Push notifications](../../concepts/push-notifications) for setup.
## Request
No parameters. Pass `X-Company-Id` if your API key has access to multiple companies.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/push/settings" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"push": {
"web": {
"enabled": true,
"vapidPublicKey": "BH3tXU0JSzok9ND7DyeQRcEy..."
},
"ios": {
"configured": true,
"keyId": "ABC123DEFG",
"teamId": "DEF123GHIJ",
"bundleId": "com.example.app",
"environment": "production",
"lastError": null,
"lastErrorAt": null
},
"android": {
"configured": false,
"projectId": null,
"clientEmail": null,
"lastError": null,
"lastErrorAt": null
},
"defaultIconUrl": "https://cdn.example.com/icon-192.png",
"brandIconUrl": "https://cdn.example.com/logo.png",
"devices": {
"active": {
"web": 1204,
"ios": 312,
"android": 0
},
"identifiedActive": 980,
"totalActive": 1516
}
},
"message": "Push is ready for web, iOS."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: account:read"
}
```
# Send a Test Push
Source: https://docs.sequenzy.com/api-reference/push/test
POST /api/v1/push/test
Send a test push to a device or a contact's devices
Sends a real push to one device (`deviceId`) or to every active device of a contact (`email` or `subscriberId`). Provide `title`, `body`, or both. Tests are excluded from stats and limited to 200 per company in a rolling 24-hour window. Requires the `campaigns:send` scope.
## Request
One device to notify.
Notify every active device of this contact.
Notify every active device of this subscriber.
Notification title, up to 120 characters. Merge tags such as `{{FIRST_NAME|there}}` work.
Notification message, up to 500 characters. Merge tags work.
Link opened on tap: an https URL, an app deep link (`myapp://...`), or a merge
tag. Browsers only open https links. `null` clears it.
Optional https image shown in the notification.
Optional https icon for web push. Defaults to the icon in push settings.
Limit delivery to `web`, `ios`, and/or `android`. Omit or pass `[]` for every
platform.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/test" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"email":"you@example.com","title":"Test","body":"Hello from Sequenzy"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"pushSendId": "push_abc",
"deviceCount": 2,
"remainingToday": 198,
"message": "Test push queued for 2 devices."
}
```
```json 400 theme={null}
{
"error": "This contact has no active device on a platform that is set up."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:send"
}
```
```json 404 theme={null}
{
"error": "Device not found."
}
```
```json 429 theme={null}
{
"error": "Test push limit reached (200 per 24 hours). Try again later."
}
```
# Unschedule a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/unschedule-campaign
POST /api/v1/push/campaigns/{campaignId}/unschedule
Move a scheduled push campaign back to draft
Moves a scheduled push campaign back to draft. The queued send is discarded. Requires the `campaigns:send` scope.
## Request
The push campaign ID.
No request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123/unschedule" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "draft",
"content": {
"title": "24h sale",
"body": "30% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "This campaign is not a push campaign."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:send"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
```json 409 theme={null}
{
"error": "Cannot unschedule a campaign in \"sent\" status."
}
```
# Update a Push Campaign
Source: https://docs.sequenzy.com/api-reference/push/update-campaign
PATCH /api/v1/push/campaigns/{campaignId}
Edit a draft or scheduled push campaign
Updates a draft or scheduled push campaign. Only the fields you pass change; content fields merge over the stored content. A scheduled campaign keeps its schedule, and cannot have its content cleared; unschedule it first. Requires the `campaigns:write` scope.
## Request
The push campaign ID.
Campaign name.
Audience, same shape as campaign `targetLists`.
Notification title, up to 120 characters. Merge tags such as `{{FIRST_NAME|there}}` work.
Notification message, up to 500 characters. Merge tags work.
Link opened on tap: an https URL, an app deep link (`myapp://...`), or a merge
tag. Browsers only open https links. `null` clears it.
Optional https image shown in the notification.
Optional https icon for web push. Defaults to the icon in push settings.
Limit delivery to `web`, `ios`, and/or `android`. Omit or pass `[]` for every
platform.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/push/campaigns/cmp_abc123" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"body":"40% off everything"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"campaign": {
"id": "cmp_abc123",
"name": "Flash sale",
"type": "push",
"status": "draft",
"content": {
"title": "24h sale",
"body": "40% off everything",
"url": "https://shop.example.com/sale",
"imageUrl": null,
"iconUrl": null,
"platforms": [
"web",
"ios"
]
},
"targetLists": {
"type": "all"
},
"scheduledAt": null,
"sentAt": null,
"estimatedRecipientCount": null,
"labelIds": [],
"createdAt": "2026-10-01T12:00:00.000Z",
"updatedAt": "2026-10-01T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"error": "name must be between 1 and 255 characters."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: campaigns:write"
}
```
```json 404 theme={null}
{
"error": "Campaign not found."
}
```
```json 409 theme={null}
{
"error": "Only draft or scheduled push campaigns can be edited."
}
```
# Update Web Push Settings
Source: https://docs.sequenzy.com/api-reference/push/web-settings
PUT /api/v1/push/settings/web
Turn web push on or off and set the default icon
Turns web push on or off, or sets the default notification icon. The first time you turn it on, Sequenzy generates your workspace's VAPID keys. Turning it off keeps the keys, so browsers that already subscribed keep working when you turn it on again.
Requires the `integrations:manage` scope and edit access to the workspace.
## Request
Turn web push on or off. Provide `enabled`, `defaultIconUrl`, or both.
https icon used when a message has no icon of its own. `null` or an empty
string clears it, and web pushes go back to your company logo.
When `enabled` is `true`, the response also includes `siteKey`: the active
[web tracking key](../web-tracking-keys/list) web push uses, with its install
snippet. If the workspace has no active key and your company website is set,
one named `Website` is created for that website and its `www` or apex twin, and
`siteKeyCreated` is `true`. `siteKey` is `null` when there is no key and the
website is unknown or not a valid origin. Requests that do not enable web push
omit both fields.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/push/settings/web" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"enabled": true}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"push": {
"web": {
"enabled": true,
"vapidPublicKey": "BH3tXU0JSzok9ND7DyeQRcEy..."
},
"ios": {
"configured": true,
"keyId": "ABC123DEFG",
"teamId": "DEF123GHIJ",
"bundleId": "com.example.app",
"environment": "production",
"lastError": null,
"lastErrorAt": null
},
"android": {
"configured": false,
"projectId": null,
"clientEmail": null,
"lastError": null,
"lastErrorAt": null
},
"defaultIconUrl": "https://cdn.example.com/icon-192.png"
},
"siteKey": {
"id": "wtk_123",
"name": "Website",
"publicKey": "seq_pk_abc123",
"allowedOrigins": ["https://example.com", "https://www.example.com"],
"installSnippet": ""
},
"siteKeyCreated": true,
"message": "Web push is enabled. Created site key seq_pk_abc123 for https://example.com, https://www.example.com."
}
```
```json 400 theme={null}
{
"error": "defaultIconUrl must be an https:// URL."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
# Request a Brand
Source: https://docs.sequenzy.com/api-reference/references/brand-requests-create
POST /api/v1/gallery/brand-requests
Ask for a brand, like a competitor, to be added to the email gallery
Ask for a brand, usually a competitor, to be added to the [Sequenzy email gallery](https://sequenzy.com/email-examples), the same as **Suggest a brand** in the dashboard's gallery picker. Sequenzy signs up to the brand's emails and adds them to the gallery; follow the request with [List Brand Requests](/api-reference/references/brand-requests-list).
Asking again for the same domain keeps the one request: it returns 200 with `created` set to `false`, and a new `note` replaces the old one, so a retry never duplicates. If the brand is already in the gallery, the request comes back with status `available`. Your company can keep up to 50 requests.
Requires the `templates:write` scope.
## Request
The brand's website or domain, up to 255 characters, such as `competitor.com`
or `https://competitor.com/pricing`. It is reduced to the registrable domain,
so `www.competitor.com` and `competitor.com` are the same request. Your own
website returns 400.
Optional, up to 300 characters: what you want to see from the brand, such as
its onboarding emails.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/gallery/brand-requests" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"website": "competitor.com",
"note": "Their onboarding emails"
}'
```
## Response
`true` for a new request (status 201), `false` when you had already requested
this domain (status 200).
The request, with the same fields as in [List Brand
Requests](/api-reference/references/brand-requests-list).
What happens next.
```json 201 theme={null}
{
"success": true,
"created": true,
"request": {
"id": "req_3jd9",
"domain": "competitor.com",
"note": "Their onboarding emails",
"status": "requested",
"declineReason": null,
"requestedAt": "2026-10-02T15:40:00.000Z",
"brand": null
},
"message": "Requested. The brand moves to collecting once it is added, and its emails show up in references once they arrive."
}
```
```json 400 theme={null}
{
"error": "That is your own website. Enter a brand you want to learn from."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:write"
}
```
A 400 also means the website is not a valid domain, or that your company already keeps 50 requests; remove some with [Remove a Brand Request](/api-reference/references/brand-requests-delete) first. A 422 means the body failed validation, such as a missing `website` or a `note` over 300 characters.
# Remove a Brand Request
Source: https://docs.sequenzy.com/api-reference/references/brand-requests-delete
DELETE /api/v1/gallery/brand-requests/{id}
Withdraw one of your requests to add a brand to the email gallery
Withdraw one of your brand requests. A brand that was already added to the gallery stays there.
Requires the `templates:write` scope.
The request ID, from [List Brand
Requests](/api-reference/references/brand-requests-list).
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/gallery/brand-requests/req_3jd9" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"deleted": true,
"id": "req_3jd9"
}
```
```json 404 theme={null}
{
"error": "That brand request was not found.",
"code": "BRAND_REQUEST_NOT_FOUND"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
# List Brand Requests
Source: https://docs.sequenzy.com/api-reference/references/brand-requests-list
GET /api/v1/gallery/brand-requests
Your requests to add brands, like competitors, to the email gallery
List your requests to add brands, usually competitors, to the [Sequenzy email gallery](https://sequenzy.com/email-examples), newest first, with where each one stands. These are the requests from **Suggest a brand** in the dashboard's gallery picker. Requests are private to your company.
Each request moves from `requested` to `collecting` once the brand is added to the gallery, then to `available` once its emails are in. Then `brand.url` links to them, and [List Email References](/api-reference/references/list) can return them.
Requires the `templates:read` scope.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/gallery/brand-requests" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
Your requests, newest first.
Request ID.The brand's registrable domain, such as `competitor.com`.What you asked to see from the brand.`requested` (not in the gallery yet), `collecting` (added, emails being collected), `available` (its emails are in the gallery) or `declined`.Why the brand won't be added, when declined.When you asked (ISO 8601).Once `available`: the gallery brand's `name`, `slug` and `url`.
How many requests your company can keep, declined ones included.
```json 200 theme={null}
{
"success": true,
"requests": [
{
"id": "req_8fk2",
"domain": "linear.app",
"note": "Their onboarding emails",
"status": "available",
"declineReason": null,
"requestedAt": "2026-10-01T09:12:00.000Z",
"brand": {
"name": "Linear",
"slug": "linear",
"url": "https://sequenzy.com/email-examples/brands/linear"
}
},
{
"id": "req_3jd9",
"domain": "competitor.com",
"note": null,
"status": "requested",
"declineReason": null,
"requestedAt": "2026-10-02T15:40:00.000Z",
"brand": null
}
],
"limit": 50
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:read"
}
```
# List Email References
Source: https://docs.sequenzy.com/api-reference/references/list
GET /api/v1/references
Gallery emails from brands like yours, or the whole gallery, to model a new email on
List real emails from the [Sequenzy email gallery](https://sequenzy.com/email-examples) to model a new email on, for the kind of email you are creating. By default they come from the gallery brands most like you: Sequenzy compares your company description by meaning with a short profile of each brand: what it sells, and to whom. Your description is compared on each request (a ranking is reused for up to 10 minutes), so a change to it counts right away. New gallery brands join the ranking once profiled, usually within six hours. With `scope=all`, list the whole gallery for that kind of email instead. These are the examples in the **Gallery** tab of the dashboard's new campaign, transactional and sequence pickers, with its **Brands like yours** and **All examples** switch.
Each email's `url` works with [Create Template from Example](/api-reference/templates/create-from-example), and each sequence's with [Create Sequence from Example](/api-reference/sequences/create-from-example).
Missing a brand you want to learn from? [Request it](/api-reference/references/brand-requests-create) to have it added to the gallery.
Requires the `templates:read` scope.
## Request
`campaign` (promotions, newsletters and product updates), `sequence`
(lifecycle emails, plus whole sequences) or `transactional`.
`similar`: emails (and, for `kind=sequence`, whole sequences) from the 12
gallery brands most like you, closest first. `all`: the whole gallery for the
`kind`, newest first, 48 per page.
Search words, up to 80 characters, matched by meaning, most relevant first.
With `scope=similar` only those brands' emails are searched. A search is paged
like `scope=all`.
Page for `scope=all` or a search, from 1 to 200.
For `scope=similar` without `q`: a gallery subtype to list first, such as
`password_reset` or `welcome`. Ignored when it does not belong to the `kind`.
For `scope=similar` without `q`: most emails to return, from 1 to 60. Also
caps how many sequences are returned.
## Response
Always `true` on success.
The requested `kind`.
`similar` or `all`.
This page. `scope=similar` without a search is always one page.
How many pages there are, at most 200.
`ready`, or `missing` when your company has neither a description nor company
context to compare with gallery brands yet. `scope=similar` is then empty; add
a description in your dashboard settings, or use `scope=all`. Always `ready`
for `scope=all`, which does not use it.
With `scope=similar`, the gallery brands most like you, closest first: `name`,
`domain`, `slug` and their gallery `url`. Your own brand is never included.
Empty for `scope=all`.
Gallery emails. With `scope=similar` and no search: the asked-for subtype
first, then one email per brand in turn, closest brands first.
Gallery email ID.Subject line.Preview text.Email type, such as `transactional`.Email subtype, such as `password_reset`.When the brand sent it (ISO 8601).
The email's breakdown, `null` until it has been analyzed. Counts come from
its HTML; `voice`, `language`, `structure` and `takeaways` come from AI
reading the copy. Any field is `null`, or an empty list, when unknown.
Label of the first button, the primary call to action.Button labels in reading order, up to 5.Button-styled links.Links of any kind, buttons included.Visible words.Estimated reading time.Content images; icons and spacers are left out.Whether it uses a GIF.Whether it ships custom `prefers-color-scheme` dark styles.Most used font families, up to 3.`single` or `multi`.`plain`, `text_heavy`, `balanced` or `visual`.`friendly`, `professional`, `playful`, `urgent`, `inspirational`, `informative` or `personal`.BCP 47 language code, such as `en`.The email's beats in order, such as "Logo header" and "Primary button".Why the email works, as observations about its choices. Never performance claims.The sender as shown on gallery pages, with personal details redacted: `name` and `address`, either may be `null`. `null` when unknown.The sender: `name`, `domain` and `slug`.Gallery page for the email, or for an email in a sequence its sequence page ending in `#email-{slug}`. Either is accepted by Create Template from Example.
For `kind=sequence` on the first page only: whole gallery sequences with `id`,
`name`, `emailCount`, `brand` and `url`. With `scope=similar`, closest brands
first; with a search, the sequences of the best matches. Empty otherwise.
```json 200 theme={null}
{
"success": true,
"kind": "transactional",
"scope": "similar",
"page": 1,
"pageCount": 1,
"profile": "ready",
"brands": [
{
"name": "Linear",
"domain": "linear.app",
"slug": "linear",
"url": "https://sequenzy.com/email-examples/brands/linear"
}
],
"emails": [
{
"id": "g_4kq9Lw",
"subject": "Reset your Linear password",
"preheader": null,
"type": "transactional",
"subtype": "password_reset",
"sentAt": "2026-09-14T10:02:00.000Z",
"analysis": {
"mainButton": "Reset password",
"buttonLabels": ["Reset password"],
"buttonCount": 1,
"linkCount": 3,
"wordCount": 64,
"readingTimeSeconds": 17,
"imageCount": 1,
"hasGif": false,
"darkMode": false,
"fonts": ["Inter"],
"columns": "single",
"format": "visual",
"voice": "professional",
"language": "en",
"structure": ["Logo header", "One-line explanation", "Primary button", "Security note"],
"takeaways": [
"The button is the only call to action, so the next step is unmistakable."
],
"sender": { "name": "Linear", "address": "notifications@linear.app" }
},
"brand": { "name": "Linear", "domain": "linear.app", "slug": "linear" },
"url": "https://sequenzy.com/email-examples/brands/linear/emails/reset-your-linear-password"
}
],
"sequences": []
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:read"
}
```
```json 503 theme={null}
{
"error": "Similar brands are unavailable right now. Try again shortly.",
"code": "REFERENCES_UNAVAILABLE"
}
```
The lists are empty when the email gallery is not available, or with `scope=similar` when your company has no description yet (`profile` is `missing`). With `scope=similar` and no `q`, every result is on page 1, so later pages are empty. A 503 with code `REFERENCES_UNAVAILABLE` means your description could not be compared with gallery brands right now; nothing changed, so retry shortly or use `scope=all`. A 422 means a parameter failed validation, such as an unknown `kind` or `scope`, or a `limit` over 60.
# Watch a Brand
Source: https://docs.sequenzy.com/api-reference/references/watchlist-create
POST /api/v1/gallery/watchlist
Start watching a brand's emails, like a competitor's
Watch a brand, usually a competitor, by its website, the same as **Watch a brand** on the dashboard's **Competitors** page.
If the brand is already in the [Sequenzy email gallery](https://sequenzy.com/email-examples), you watch it at once: the watch comes back `available`, and its emails are in [List Watched Brand Emails](/api-reference/references/watchlist-emails). Otherwise the brand is requested, the same as [Request a Brand](/api-reference/references/brand-requests-create), and its emails show up once it is added and they are collected. New emails usually arrive a few hours after they are sent.
Watching again keeps the one watch: it returns 200 with `created` set to `false`, and a new `note` replaces the old one, so a retry never duplicates. Your company can keep up to 50 watches and brand requests together.
Requires the `templates:write` scope.
## Request
The brand's website or domain, up to 255 characters, such as `competitor.com`
or `https://competitor.com/pricing`. It is reduced to the registrable domain,
so `www.competitor.com` and `competitor.com` are the same brand. Your own
website returns 400.
Optional, up to 300 characters: what you want to see from the brand, such as
its onboarding emails.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/gallery/watchlist" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "website": "linear.app" }'
```
## Response
`true` for a new watch (status 201), `false` when you already watched this
domain (status 200).
The watch, with the same fields as in [List Watched
Brands](/api-reference/references/watchlist-list).
What happens next.
```json 201 theme={null}
{
"success": true,
"created": true,
"watch": {
"id": "req_8fk2",
"domain": "linear.app",
"note": null,
"status": "available",
"declineReason": null,
"watchedAt": "2026-10-04T10:00:00.000Z",
"emailCount": 42,
"latestEmailAt": "2026-10-03T16:05:00.000Z",
"brand": {
"name": "Linear",
"slug": "linear",
"url": "https://sequenzy.com/email-examples/brands/linear"
}
},
"message": "Watching. Its emails are in GET /gallery/watchlist/emails, and new ones arrive a few hours after they're sent."
}
```
```json 400 theme={null}
{
"error": "That is your own website. Enter a brand you want to learn from."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:write"
}
```
A 400 also means the website is not a valid domain, or that your company already keeps 50 watches and brand requests; stop watching some with [Stop Watching a Brand](/api-reference/references/watchlist-delete) first. A 422 means the body failed validation, such as a missing `website` or a `note` over 300 characters.
# Stop Watching a Brand
Source: https://docs.sequenzy.com/api-reference/references/watchlist-delete
DELETE /api/v1/gallery/watchlist/{id}
Stop watching a brand's emails
Stop watching one of your brands by its watch ID from [List Watched Brands](/api-reference/references/watchlist-list). This also withdraws the brand request with the same ID. The brand stays in the gallery.
Requires the `templates:write` scope.
The watch ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/gallery/watchlist/req_8fk2" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"deleted": true,
"id": "req_8fk2"
}
```
```json 404 theme={null}
{
"error": "That watched brand was not found.",
"code": "WATCHED_BRAND_NOT_FOUND"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:write"
}
```
# List Watched Brand Emails
Source: https://docs.sequenzy.com/api-reference/references/watchlist-emails
GET /api/v1/gallery/watchlist/emails
Emails the brands you watch have sent, newest first
List emails from the brands your company watches, newest sent first, a page at a time. This is the feed on the dashboard's **Competitors** page. Emails usually arrive a few hours after they are sent, and `since` matches the send time. When you poll, set `since` about a day before your last check and skip emails whose `id` you have already seen.
Each email's `url` also works as the `url` of [Create Template from Example](/api-reference/templates/create-from-example), to remix it for your brand.
Requires the `templates:read` scope.
## Query parameters
Only this watched brand, by website or domain, such as `linear.app`. A brand
you don't watch returns an empty list.
Only emails sent at or after this ISO 8601 date or time, such as `2026-10-01`
or `2026-10-01T00:00:00Z`.
Emails per page, 1 to 60.
`nextCursor` from the previous page, for older emails. Pages don't shift when
new emails arrive while you page.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/gallery/watchlist/emails?since=2026-10-01&limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
Emails from your watched brands, newest sent first.
Email ID.Subject line.Preview text.Email type, such as `promotional`, `lifecycle` or `product_update`.Email subtype, such as `feature_announcement` or `win_back`.When the brand sent it (ISO 8601).When the gallery received it (ISO 8601).The email's breakdown, the same as in [List Email References](/api-reference/references/list). `null` until it has been analyzed.The brand's `name`, `domain` and `slug`.The email's gallery page.
Pass as `cursor` for the next page. `null` on the last page.
```json 200 theme={null}
{
"success": true,
"emails": [
{
"id": "em_51kd",
"subject": "Introducing Linear Asks",
"preheader": "Turn requests from Slack into issues",
"type": "product_update",
"subtype": "feature_announcement",
"sentAt": "2026-10-03T16:05:00.000Z",
"collectedAt": "2026-10-03T21:40:00.000Z",
"analysis": null,
"brand": { "name": "Linear", "domain": "linear.app", "slug": "linear" },
"url": "https://sequenzy.com/email-examples/brands/linear/emails/introducing-linear-asks"
}
],
"nextCursor": "2026-10-03T16:05:00.000Z_em_51kd"
}
```
```json 400 theme={null}
{
"error": "since must be an ISO 8601 date, like 2026-10-01T00:00:00Z"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:read"
}
```
A 400 also means `domain` is not a website or `cursor` is not one this endpoint returned. A 422 means `limit` is outside 1 to 60.
# List Watched Brands
Source: https://docs.sequenzy.com/api-reference/references/watchlist-list
GET /api/v1/gallery/watchlist
The brands you watch, like competitors, and how many of their emails are in
List the brands your company watches, usually competitors, newest watch first. This is the dashboard's **Competitors** page. Read their emails with [List Watched Brand Emails](/api-reference/references/watchlist-emails).
A brand already in the [Sequenzy email gallery](https://sequenzy.com/email-examples) is `available`, with `emailCount` and `latestEmailAt`. A brand that isn't in the gallery yet is requested when you watch it: it moves from `requested` to `collecting` once it is added, then to `available` once its emails are in. A watch is a brand request, so [List Brand Requests](/api-reference/references/brand-requests-list) returns the same rows.
Requires the `templates:read` scope.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/gallery/watchlist" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
Your watched brands, newest watch first.
Watch ID, also the ID of its brand request.The brand's registrable domain, such as `linear.app`.What you want to see from the brand.`available` (its emails are in the gallery), `requested` (not in the gallery yet), `collecting` (added, emails being collected) or `declined`.Why the brand won't be added, when declined.When you started watching (ISO 8601).The brand's emails in the gallery. `0` unless `available`.When its newest email in the gallery was sent. `null` unless `available`.Once `available`: the gallery brand's `name`, `slug` and `url`.
How many watches and brand requests your company can keep together.
```json 200 theme={null}
{
"success": true,
"watchlist": [
{
"id": "req_8fk2",
"domain": "linear.app",
"note": "Their launch emails",
"status": "available",
"declineReason": null,
"watchedAt": "2026-10-01T09:12:00.000Z",
"emailCount": 42,
"latestEmailAt": "2026-10-03T16:05:00.000Z",
"brand": {
"name": "Linear",
"slug": "linear",
"url": "https://sequenzy.com/email-examples/brands/linear"
}
},
{
"id": "req_3jd9",
"domain": "competitor.com",
"note": null,
"status": "requested",
"declineReason": null,
"watchedAt": "2026-10-02T15:40:00.000Z",
"emailCount": 0,
"latestEmailAt": null,
"brand": null
}
],
"limit": 50
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: templates:read"
}
```
# Get Segment Count
Source: https://docs.sequenzy.com/api-reference/segments/count
GET /api/v1/segments/{segmentId}/count
Preview the active subscriber count for a segment
Get the number of active subscribers who currently match a saved segment.
Each request evaluates the saved filters again. The `count` matches
`activeSubscriberCount` from [List Segments](/api-reference/segments/list)
when the underlying subscriber data is unchanged.
Custom-attribute filters use asynchronously synced values. After an import,
allow roughly 30–35 seconds for attribute updates to sync, or longer when
background processing is busy, then request the count again. An import with
status `completed` can still be waiting for this sync; recreating the segment
does not accelerate it.
A segment can use the `segment` filter to include or exclude another saved
segment, and that segment can reference others in turn. Counting follows up
to 16 levels of these references and up to 1,000 references in total, counting
a segment again each time it is referenced. Nested filter groups inside a
segment do not count toward either limit. Past a limit, the request returns
`400` with code `SEGMENT_REFERENCE_DEPTH_EXCEEDED` or
`SEGMENT_REFERENCE_COUNT_EXCEEDED`. `details` names the segment that
exceeded the limit and the chain of segment IDs followed to reach it.
Retrying does not help. For too-deep chains, copy the conditions of an
inner segment into its parent. For too many references, reference fewer
segments or copy the conditions of shared segments into the segments that
use them.
## Request
Segment ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/segments/seg_active_non_buyers/count" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"segmentId": "seg_active_non_buyers",
"count": 1180
}
```
```json 400 theme={null}
{
"success": false,
"error": "Segment references are nested too deeply. A segment filter can reference other segments up to 16 levels deep, and the reference to segment seg_r would be level 17 (chain: seg_b > seg_c > ... > seg_q > seg_r). Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"code": "SEGMENT_REFERENCE_DEPTH_EXCEEDED",
"title": "Segment references nested too deeply",
"description": "Segment references are nested too deeply. A segment filter can reference other segments up to 16 levels deep, and the reference to segment seg_r would be level 17 (chain: seg_b > seg_c > ... > seg_q > seg_r). Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"howToFix": "Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"docsUrl": "https://docs.sequenzy.com/api-reference/segments/count",
"details": {
"limit": "depth",
"segmentId": "seg_r",
"referenceChain": [
"seg_b",
"seg_c",
"seg_d",
"seg_e",
"seg_f",
"seg_g",
"seg_h",
"seg_i",
"seg_j",
"seg_k",
"seg_l",
"seg_m",
"seg_n",
"seg_o",
"seg_p",
"seg_q"
],
"maxDepth": 16,
"maxReferences": 1000
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Segment not found"
}
```
# Create Segment
Source: https://docs.sequenzy.com/api-reference/segments/create
POST /api/v1/segments
Create a saved subscriber segment
Create a segment from either the legacy flat filter array or a nested `root` filter group. You must provide exactly one of `filters` or `root`, and it must contain at least one filter.
## Request
Segment name.
Legacy v1 filter array. Use with `filterJoinOperator`.
`and` requires every v1 filter to match. `or` matches any v1 filter.
Nested v2 filter group. Use this for nested AND/OR logic, event filters, or
segment filters. Groups need `kind: "group"` and a `children` array. Filters
need `field`, `operator` and a string `value`; omit `value` only for
`is_empty` and `is_not_empty`. A child without `kind` is read as a filter.
A `segment` filter matches the members of another saved segment. Counting
follows up to 16 levels of segment-to-segment references and 1,000
references in total; past either limit, counting the segment returns
`SEGMENT_REFERENCE_DEPTH_EXCEEDED` or `SEGMENT_REFERENCE_COUNT_EXCEEDED`.
Stripe trial subfilters stay under `field: "stripeTrialProduct"`:
`prod_123:is_canceled`, `prod_123:end_at:2026-05-26`, or
`prod_123:start_at:7 days ago`.
Every filter field validates its own operator set:
* `status`, `segment`, `smsStatus`: `is`, `is_not`
* `phone`: `is_empty`, `is_not_empty`
* `tag`: `contains`, `not_contains`, `is_empty`, `is_not_empty`
* `email`: `contains`, `not_contains` (domain or substring), `is`, `is_not` (exact, case-insensitive address)
* `emailProvider`, `list`: `is`, `is_not`, `is_empty`, `is_not_empty`
* `firstName`, `lastName`: `contains`, `not_contains`, `is_empty`, `is_not_empty`
* `added`: `less_than`, `more_than`
* `attribute`: equality, empty checks, numeric/date comparisons, and contains checks
* `event`, email engagement fields: `is`, `is_not`, `at_least`, `less_than_count`
* `emailBounced`: also supports `is_temporary_bounce`, `is_permanent_bounce`
* Stripe product fields: product-specific purchase/current/trial/date operators
* `commerceProduct`, `commerceCollection`: `is`, `is_not`, `at_least`, `less_than_count`
For `smsStatus`, use `subscribed`, `unsubscribed`, `pending`, or `not_subscribed`.
Phone presence filters use an empty `value` string. Commerce values use
`provider:productId` or `provider:collectionKey`, for example `shopify:42` or
`shopify:summer`. Count operators append an order threshold, for example
`shopify:42:3`; omitting the threshold means 1. A bare product ID or collection
key matches across providers. Prefer provider-qualified values to avoid
collisions between stores.
Email engagement values can target any email (`30d`, `all`), one campaign
(`campaign:`), or a delivery policy. Use `marketing:`
for marketing-policy campaigns, automations, and Send API traffic, and
`transactional:` for transactional sends. For example,
`{"field":"emailOpened","operator":"is","value":"marketing:all"}` matches
contacts who have ever opened a marketing email. Policy scopes work with
presence and bounce-subtype operators, not count operators.
Attribute filter names are matched case-sensitively against synced subscriber
attributes. When a filter references a custom attribute no subscriber has a
synced value for, the segment is still created but the response includes a
`warnings` array explaining that the filter currently matches no subscribers
(or every subscriber for exclusion operators like `is_empty`), with a
casing suggestion when a close match exists.
For arrays of objects, use wildcard paths such as
`history_events[].eventvenue_id:2103`. Two or more positive attribute filters
on the same array path that are connected only by `AND` must be satisfied by
one shared array element. A venue match on one history entry cannot combine
with a date match on another entry. Negative filters (`is_not`,
`not_contains`, and `is_empty`) remain independent because each asserts that
no array element matches its condition.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/segments" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Active non-buyers",
"root": {
"kind": "group",
"id": "root",
"joinOperator": "and",
"children": [
{
"kind": "filter",
"id": "filter-1",
"field": "attribute",
"operator": "gte",
"value": "last_login_days_ago:90"
},
{
"kind": "group",
"id": "group-1",
"joinOperator": "or",
"children": [
{
"kind": "filter",
"id": "filter-2",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
},
{
"kind": "filter",
"id": "filter-3",
"field": "segment",
"operator": "is_not",
"value": "seg_paying_customers"
}
]
}
]
}
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"segment": {
"id": "seg_active_non_buyers",
"name": "Active non-buyers",
"filters": [
{
"id": "filter-1",
"field": "attribute",
"operator": "gte",
"value": "last_login_days_ago:90"
},
{
"id": "filter-2",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
},
{
"id": "filter-3",
"field": "segment",
"operator": "is_not",
"value": "seg_paying_customers"
}
],
"filterJoinOperator": "and",
"format": "v2",
"root": {
"kind": "group",
"id": "root",
"joinOperator": "and",
"children": [
{
"kind": "filter",
"id": "filter-1",
"field": "attribute",
"operator": "gte",
"value": "last_login_days_ago:90"
},
{
"kind": "group",
"id": "group-1",
"joinOperator": "or",
"children": [
{
"kind": "filter",
"id": "filter-2",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
},
{
"kind": "filter",
"id": "filter-3",
"field": "segment",
"operator": "is_not",
"value": "seg_paying_customers"
}
]
}
]
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide either root or filters, not both"
}
```
A malformed `root` or `filters` returns `400` with `issues`, one entry per failing path:
```json 400 theme={null}
{
"success": false,
"error": "Invalid segment filters: root.children[1].children: must be an array of filters and groups",
"issues": [
{
"path": "root.children[1].children",
"message": "must be an array of filters and groups"
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Segment must have at least one filter"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Operator \"is not\" is not supported for Tag filters. Use one of: contains, does not contain, is empty, is not empty."
}
```
```json 409 theme={null}
{
"success": false,
"code": "SEGMENT_NAME_ALREADY_EXISTS",
"error": "Segment name already exists",
"title": "Segment name already exists",
"description": "A saved segment named \"Active non-buyers\" already exists in this company. Segment names are unique per company, so another create_segment call with this exact name will fail.",
"resolution": "Use the existing segment id \"seg_active_non_buyers\", call list_segments before creating, or retry create_segment with a different name.",
"docsUrl": "https://docs.sequenzy.com/api-reference/segments/create",
"details": {
"segmentName": "Active non-buyers",
"existingSegment": {
"id": "seg_active_non_buyers",
"name": "Active non-buyers"
}
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Delete Segment
Source: https://docs.sequenzy.com/api-reference/segments/delete
DELETE /api/v1/segments/{segmentId}
Delete a saved subscriber segment
Delete a saved segment. Subscribers matched by the segment are not affected, only the saved filter definition is removed. This action cannot be undone.
## Request
Segment ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/segments/seg_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Segment not found"
}
```
# List Segments
Source: https://docs.sequenzy.com/api-reference/segments/list
GET /api/v1/segments
List saved segments and their subscriber counts
List the saved segments in your company.
Counts are recalculated from each segment's saved filters on every request.
`subscriberCount` includes matching contacts of every status;
`activeSubscriberCount` includes only active contacts and matches
[Get Segment Count](/api-reference/segments/count) when the underlying data
is unchanged. Custom-attribute updates sync asynchronously, so counts can
take roughly 30–35 seconds or longer to reflect an import, even after its
status becomes `completed`.
A segment can use the `segment` filter to include or exclude another saved
segment, and that segment can reference others in turn. Counting follows up
to 16 levels of these references and up to 1,000 references in total, counting
a segment again each time it is referenced. Nested filter groups inside a
segment do not count toward either limit. Past a limit, the request returns
`400` with code `SEGMENT_REFERENCE_DEPTH_EXCEEDED` or
`SEGMENT_REFERENCE_COUNT_EXCEEDED`. `details` names the segment that
exceeded the limit and the chain of segment IDs followed to reach it.
Retrying does not help. For too-deep chains, copy the conditions of an
inner segment into its parent. For too many references, reference fewer
segments or copy the conditions of shared segments into the segments that
use them.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/segments" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"segments": [
{
"id": "seg_active_buyers",
"name": "Active buyers",
"filters": [
{
"id": "filter-1",
"field": "tag",
"operator": "contains",
"value": "customer"
}
],
"filterJoinOperator": "and",
"format": "v1",
"createdAt": "2026-04-21T10:00:00.000Z",
"subscriberCount": 1200,
"activeSubscriberCount": 1180
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Segment references are nested too deeply. A segment filter can reference other segments up to 16 levels deep, and the reference to segment seg_r would be level 17 (chain: seg_b > seg_c > ... > seg_q > seg_r). Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"code": "SEGMENT_REFERENCE_DEPTH_EXCEEDED",
"title": "Segment references nested too deeply",
"description": "Segment references are nested too deeply. A segment filter can reference other segments up to 16 levels deep, and the reference to segment seg_r would be level 17 (chain: seg_b > seg_c > ... > seg_q > seg_r). Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"howToFix": "Retrying does not help. Copy the conditions of an inner segment into its parent instead of referencing it to reduce the nesting.",
"docsUrl": "https://docs.sequenzy.com/api-reference/segments/count",
"details": {
"limit": "depth",
"segmentId": "seg_r",
"referenceChain": [
"seg_b",
"seg_c",
"seg_d",
"seg_e",
"seg_f",
"seg_g",
"seg_h",
"seg_i",
"seg_j",
"seg_k",
"seg_l",
"seg_m",
"seg_n",
"seg_o",
"seg_p",
"seg_q"
],
"maxDepth": 16,
"maxReferences": 1000
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Update Segment
Source: https://docs.sequenzy.com/api-reference/segments/update
PATCH /api/v1/segments/{segmentId}
Update a saved subscriber segment
Update a segment name or filter definition. You can send either legacy `filters` or a nested `root`, not both.
## Request
Segment ID.
New segment name.
Legacy v1 filter array.
`and` or `or` for legacy v1 filters.
Nested v2 filter group for nested logic, event filters, or segment filters.
Groups need `kind: "group"` and a `children` array. Filters need `field`,
`operator` and a string `value`; omit `value` only for `is_empty` and
`is_not_empty`. A child without `kind` is read as a filter.
Stripe trial subfilters stay under `field: "stripeTrialProduct"`:
`prod_123:is_canceled`, `prod_123:end_at:2026-05-26`, or
`prod_123:start_at:7 days ago`.
Every filter field validates its own operator set:
* `status`, `segment`, `smsStatus`: `is`, `is_not`
* `phone`: `is_empty`, `is_not_empty`
* `tag`: `contains`, `not_contains`, `is_empty`, `is_not_empty`
* `email`: `contains`, `not_contains` (domain or substring), `is`, `is_not` (exact, case-insensitive address)
* `emailProvider`, `list`: `is`, `is_not`, `is_empty`, `is_not_empty`
* `firstName`, `lastName`: `contains`, `not_contains`, `is_empty`, `is_not_empty`
* `added`: `less_than`, `more_than`
* `attribute`: equality, empty checks, numeric/date comparisons, and contains checks
* `event`, email engagement fields: `is`, `is_not`, `at_least`, `less_than_count`
* `emailBounced`: also supports `is_temporary_bounce`, `is_permanent_bounce`
* Stripe product fields: product-specific purchase/current/trial/date operators
* `commerceProduct`, `commerceCollection`: `is`, `is_not`, `at_least`, `less_than_count`
For `smsStatus`, use `subscribed`, `unsubscribed`, `pending`, or `not_subscribed`.
Phone presence filters use an empty `value` string. Commerce values use
`provider:productId` or `provider:collectionKey`, for example `shopify:42` or
`shopify:summer`. Count operators append an order threshold, for example
`shopify:42:3`; omitting the threshold means 1. A bare product ID or collection
key matches across providers. Prefer provider-qualified values to avoid
collisions between stores.
Email engagement values can target any email (`30d`, `all`), one campaign
(`campaign:`), or a delivery policy. Use `marketing:`
or `transactional:`; for example,
`{"field":"emailOpened","operator":"is","value":"marketing:all"}` matches
contacts who have ever opened a marketing email. Policy scopes work with
presence and bounce-subtype operators, not count operators.
When updated filters reference a custom attribute no subscriber has a synced
value for, the update still succeeds but the response includes a `warnings`
array explaining that the filter currently matches no subscribers (or every
subscriber for exclusion operators like `is_empty`), with a casing suggestion
when a close match exists.
For arrays of objects, use wildcard paths such as
`history_events[].eventvenue_id:2103`. Two or more positive attribute filters
on the same array path that are connected only by `AND` must be satisfied by
one shared array element. A venue match on one history entry cannot combine
with a date match on another entry. Negative filters (`is_not`,
`not_contains`, and `is_empty`) remain independent because each asserts that
no array element matches its condition.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/segments/seg_active_non_buyers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Active non-buyers - 30 days",
"root": {
"kind": "group",
"id": "root",
"joinOperator": "and",
"children": [
{
"kind": "filter",
"id": "filter-1",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
}
]
}
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"segment": {
"id": "seg_active_non_buyers",
"name": "Active non-buyers - 30 days",
"filters": [
{
"id": "filter-1",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
}
],
"filterJoinOperator": "and",
"format": "v2",
"root": {
"kind": "group",
"id": "root",
"joinOperator": "and",
"children": [
{
"kind": "filter",
"id": "filter-1",
"field": "event",
"operator": "is_not",
"value": "saas.purchase:30d"
}
]
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide either root or filters, not both"
}
```
A malformed `root` or `filters` returns `400` with `issues`, one entry per failing path:
```json 400 theme={null}
{
"success": false,
"error": "Invalid segment filters: root.children[1].children: must be an array of filters and groups",
"issues": [
{
"path": "root.children[1].children",
"message": "must be an array of filters and groups"
}
]
}
```
```json 400 theme={null}
{
"success": false,
"error": "Operator \"is not\" is not supported for Tag filters. Use one of: contains, does not contain, is empty, is not empty."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Segment not found"
}
```
# Delete Sender Profile
Source: https://docs.sequenzy.com/api-reference/sender-profiles/delete
DELETE /api/v1/sender-profiles/{id}
Permanently delete a sender (From) profile
Permanently delete one sender (From) profile. Requires the
`companies:manage` scope.
Deletion is refused when the profile is the company's last sender or is still
used by a live campaign, active sequence, sequence step, or transactional
email. Reassign or stop the named sending surfaces and retry.
This cannot be undone. Draft and rejected campaigns and the account default
move to the best remaining sender when needed. Review the returned
`fallbackSenderProfileId` before sending.
## Request
Sender profile ID from [List Sender
Profiles](/api-reference/sender-profiles/list).
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/sender-profiles/sender_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
ID of the profile that was permanently deleted.
Remaining profile selected for the account default and eligible draft
campaigns when reassignment was needed.
## Responses
```json 200 theme={null}
{
"success": true,
"deletedSenderProfileId": "sender_abc123",
"fallbackSenderProfileId": "sender_def456",
"message": "Sender profile deleted. The account default and eligible draft campaigns now use sender_def456 where reassignment was needed."
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: companies:manage"
}
```
```json 404 theme={null}
{
"error": "Sender profile not found"
}
```
```json 409 theme={null}
{
"error": "Cannot delete this sender profile because it is used by 1 automation step(s): Welcome sequence (node_abc123). Reassign or stop those sending surfaces first."
}
```
```json 409 theme={null}
{
"error": "Cannot delete the last sender profile. Create another sender profile first."
}
```
# List Sender Profiles
Source: https://docs.sequenzy.com/api-reference/sender-profiles/list
GET /api/v1/sender-profiles
List sender (From) and reply-to profiles with sending readiness
List the sending identities configured for a company, including which are the account defaults and whether each sender address sits on a verified sending domain.
Use the returned IDs as `senderProfileId` or `replyProfileId` when creating or updating a campaign.
SMTP submission at `smtp.sequenzy.com` sends mail into Sequenzy; it does not
configure a customer-managed outbound relay. Sequenzy handles outbound
delivery through SES or its managed MTA. External mailboxes can still be used
for Reply-To.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sender-profiles" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Sending domain backing this address, or `null` if the domain record is
missing.
Aggregate verification status of that sending domain, such as `verified` or
`pending`.
True when the domain's DNS and selected home transport are both ready.
True when this profile is the company default sender.
## Responses
```json 200 theme={null}
{
"success": true,
"senderProfiles": [
{
"id": "sender_abc123",
"name": "Acme",
"email": "hello@acme.com",
"domainId": "domain_abc123",
"domain": "acme.com",
"domainStatus": "verified",
"canSend": true,
"isDefault": true,
"createdAt": "2026-01-14T08:12:00Z"
}
],
"replyProfiles": [
{
"id": "reply_abc123",
"name": "Support",
"email": "support@acme.com",
"isDefault": true,
"createdAt": "2026-01-14T08:12:00Z"
}
],
"defaultSenderProfileId": "sender_abc123",
"defaultReplyProfileId": "reply_abc123"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
# Rename Sender Profile
Source: https://docs.sequenzy.com/api-reference/sender-profiles/update
PATCH /api/v1/sender-profiles/{id}
Rename a sender (From) profile without changing account defaults
Rename one sender (From) profile in place. Only the display name changes: the address, its sending domain, and the account-wide default From selection are left exactly as they were.
Use this to standardize a display name across the several identities one mailbox may carry - for example, consolidating `Viraj from SnapCount` down to `SnapCount`. Campaigns and sequences pinned to this profile pick up the new name on their next send.
To change *which* profile is the account default, use [Update
Company](/api-reference/companies/update) with `senderProfileId` instead. That
endpoint renames and promotes in one step; this one never touches the default.
Get profile IDs from [List Sender Profiles](/api-reference/sender-profiles/list). Requires the `companies:manage` scope.
## Request
Sender profile ID.
New display name. Trimmed before saving; must be non-empty and 255 characters
or fewer.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/sender-profiles/sender_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "SnapCount"}'
```
## Response fields
`false` when the profile already carried that name, so nothing changed. Lets a
retried call report accurately instead of claiming a change that never
happened.
Whether this profile is the account default. This endpoint never changes it.
## Responses
```json 200 theme={null}
{
"success": true,
"senderProfile": {
"id": "sender_abc123",
"name": "SnapCount",
"email": "hello@snapcount.app",
"domain": "snapcount.app",
"domainStatus": "verified",
"canSend": true,
"isDefault": true
},
"renamed": true
}
```
```json 400 theme={null}
{
"error": "name cannot be empty."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: companies:manage"
}
```
```json 404 theme={null}
{
"error": "Sender profile not found"
}
```
```json 409 theme={null}
{
"error": "Another sender identity for hello@snapcount.app is already named \"SnapCount\" (sender_def456). Rename or remove that one first, or point your campaigns at it instead."
}
```
# Rename Reply-To Profile
Source: https://docs.sequenzy.com/api-reference/sender-profiles/update-reply
PATCH /api/v1/reply-profiles/{id}
Rename a reply-to profile without changing account defaults
Rename one reply-to profile in place. Only the display name changes: the address and the account-wide default Reply-To selection are left exactly as they were.
This is the Reply-To counterpart of [Rename Sender Profile](/api-reference/sender-profiles/update). Reply-to profiles are unique per address within a company, so a rename here can never collide with a sibling profile.
To change *which* profile is the account default, use [Update
Company](/api-reference/companies/update) with `replyProfileId` instead.
Get profile IDs from [List Sender Profiles](/api-reference/sender-profiles/list). Requires the `companies:manage` scope.
## Request
Reply-to profile ID.
New display name. Trimmed before saving; must be non-empty and 255 characters
or fewer.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/reply-profiles/reply_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "SnapCount"}'
```
## Response fields
`false` when the profile already carried that name, so nothing changed.
Whether this profile is the account default. This endpoint never changes it.
## Responses
```json 200 theme={null}
{
"success": true,
"replyProfile": {
"id": "reply_abc123",
"name": "SnapCount",
"email": "support@snapcount.app",
"isDefault": true
},
"renamed": true
}
```
```json 400 theme={null}
{
"error": "name cannot be empty."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: companies:manage"
}
```
```json 404 theme={null}
{
"error": "Reply profile not found"
}
```
# Get Sending Status
Source: https://docs.sequenzy.com/api-reference/sending-status/get
GET /api/v1/sending-status
Check whether company-level sending is paused, why, and how to restore it
Check whether a workspace can send email right now. Returns the pause reason, the
sender-health rates measured against the thresholds that apply at your volume,
the automated review state, whether you can restore sending yourself, and ordered
remediation steps.
Call this whenever a send, sequence step, or test send fails for a reason that is
not a validation error. A company-level pause blocks every send, including test
sends. Requires the `account:read` scope, so a read-only key can diagnose a
blocked account.
Sender-health enforcement uses all-time totals counted from a reset watermark,
not a rolling window. `metricsWindow.expiresAt` is always `null` - a paused
rate does not decay and waiting will not restore sending. See [Sender
Health](/guides/sender-health).
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sending-status" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`active`, `paused`, or `suspended`. Anything other than `active` blocks every
send for this workspace.
The enforcement message, including the measured rate, the threshold it
crossed, and the send volume it was measured over. `null` when sending is
active.
One of `high_hard_bounce_rate`, `high_soft_bounce_rate`,
`high_complaint_rate`, `phishing_guard`, `over_email_limit`, `manual`, or
`other`. Only `high_hard_bounce_rate` can be cleared with the resume endpoint.
`over_email_limit` lifts automatically within a few minutes of your account
having email allowance again: at the next usage reset, after a plan upgrade,
or when credits are added.
ISO timestamp of the pause. `null` when sending is active.
Whether [resume](/api-reference/sending-status/resume) will succeed right now.
When `canSelfResume` is `false`, the gate that is blocking it:
`waiting_for_review`, `blocked_by_ai`, `review_failed`, `blocked_by_admin`, or
`unsupported_reason`. `null` when resume is available.
State of the automated sender-health review: `not_required`, `pending`,
`approved`, `flagged`, or `failed`.
Enforcement counts and thresholds for `hardBounce`, `softBounce`, and
`complaint`, plus their non-test send denominators: `bounceScopedSent` for
bounce rates and `complaintScopedSent` for complaint rates. `scopedSent` is a
backward-compatible alias for `bounceScopedSent`. Also includes
`enforcementMode`. `null` when the workspace has no metrics record yet or
sender-health analytics are temporarily unavailable; pause state and
remediation remain authoritative during an analytics outage.
`kind` is always `all_time_since_reset` and `expiresAt` is always `null`.
`bounceResetAt` and `complaintResetAt` are the watermarks the totals are
counted from.
`steps` for the current state, in order, plus `supportEmail`, `docsUrl`, and
`dashboardUrl`.
## Responses
```json 200 theme={null}
{
"success": true,
"status": "paused",
"pauseReason": "Bounce rate (9.910%) exceeded threshold (5%) for 111 total emails",
"pauseReasonKind": "high_hard_bounce_rate",
"pausedAt": "2026-08-05T09:12:00Z",
"selfResume": {
"canSelfResume": true,
"supported": true,
"allowedByAdmin": true,
"ownerIsTrusted": false,
"aiReviewStatus": "approved",
"aiReviewReason": null,
"aiReviewStartedAt": "2026-08-05T09:12:01Z",
"aiReviewedAt": "2026-08-05T09:12:44Z",
"unavailableReason": null
},
"senderHealth": {
"enforcementMode": "enforce",
"scopedSent": 111,
"bounceScopedSent": 111,
"complaintScopedSent": 111,
"hardBounce": {
"count": 11,
"rate": 9.91,
"warnThreshold": { "rate": 3, "count": null },
"pauseThreshold": { "rate": 5, "count": null }
},
"softBounce": {
"count": 2,
"rate": 1.802,
"warnThreshold": { "rate": 6, "count": null },
"pauseThreshold": { "rate": 10, "count": null }
},
"complaint": {
"count": 0,
"rate": 0,
"warnThreshold": { "rate": null, "count": 2 },
"pauseThreshold": { "rate": null, "count": 3 }
}
},
"metricsWindow": {
"kind": "all_time_since_reset",
"expiresAt": null,
"bounceResetAt": null,
"complaintResetAt": null,
"description": "Sender-health enforcement uses all-time totals from the reset watermark, not a rolling window."
},
"remediation": {
"steps": [
"Fix the source of the bad addresses (import, form, or integration) so no new invalid addresses enter the list.",
"Leave every permanent bounce suppressed. Removing suppressions re-sends to known-bad addresses and pauses the account again.",
"Confirm the list has been cleaned, then request a resume."
],
"supportEmail": "nic@sequenzy.com",
"docsUrl": "https://docs.sequenzy.com/guides/sender-health",
"dashboardPath": "/dashboard/company/comp_abc123",
"dashboardUrl": "https://sequenzy.com/dashboard/company/comp_abc123"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
# Resume Sending
Source: https://docs.sequenzy.com/api-reference/sending-status/resume
POST /api/v1/sending-status/resume
Restore paused sending after fixing the cause of the bounces
Restore company-level sending that was paused by a high permanent-bounce rate.
This is the supported remediation path, not a bypass: it never removes
suppressions and it enforces the same gates as the dashboard's **Resume sending**
button.
Requires the `companies:manage` scope plus owner or admin access to the company.
Only call this once the cause is actually fixed. `listSanitizationConfirmed`
is recorded on the account's audit trail as your statement that the source of
the invalid addresses is corrected and permanent bounces remain suppressed.
Check [Get Sending Status](/api-reference/sending-status/get) first. For a paused
workspace, the request only restores sending when `selfResume.canSelfResume` is
`true`, which requires a `high_hard_bounce_rate` pause, a cleared automated
sender-health review, and no admin block. An already-active workspace succeeds
as an idempotent no-op with `resumed: false`. Any other blocked state needs a
support review, and the error names which gate stopped you.
On success the bounce watermark moves to now - so the rate is recalculated from
later sends - and the service attempts to requeue paused campaigns plus due
sequence steps. If part of that queue handoff is temporarily unavailable, the
request still reports the committed active state and returns recovery guidance
in `message` rather than falsely reporting that the resume failed. Automatic
resume is available a limited number of times before a support review is
required.
## Request
Must be `true`. Confirms the source of the invalid addresses is fixed and that
permanent bounces remain suppressed.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sending-status/resume" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"listSanitizationConfirmed": true}'
```
## Response fields
`true` when this call restored sending. `false` when sending was already
active.
Sending state after the request. `active` on success.
Confirms the resume outcome. If some parked work could not be requeued, this
contains the partial-success warning and support guidance.
On success, the full [sending status](/api-reference/sending-status/get) payload
is returned alongside these fields. On rejection, the refreshed status is nested
under `details.sendingStatus` so the response can also describe the blocking
gate.
## Responses
```json 200 theme={null}
{
"success": true,
"resumed": true,
"message": "Sending resumed. Paused campaigns and due sequence steps have been requeued.",
"status": "active",
"pauseReason": null,
"pauseReasonKind": null,
"pausedAt": null,
"selfResume": {
"canSelfResume": false,
"supported": false,
"allowedByAdmin": true,
"ownerIsTrusted": false,
"aiReviewStatus": "not_required",
"aiReviewReason": null,
"aiReviewStartedAt": null,
"aiReviewedAt": null,
"unavailableReason": null
},
"metricsWindow": {
"kind": "all_time_since_reset",
"expiresAt": null,
"bounceResetAt": "2026-08-07T10:03:12Z",
"complaintResetAt": null,
"description": "Sender-health enforcement uses all-time totals from the reset watermark, not a rolling window."
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "SENDING_RESUME_BLOCKED",
"error": "Set listSanitizationConfirmed to true to confirm the bounce source is fixed and permanent bounces stay suppressed.",
"title": "Sending could not be resumed",
"docsUrl": "https://docs.sequenzy.com/guides/sender-health",
"details": { "reason": "confirmation_required" }
}
```
```json 403 theme={null}
{
"success": false,
"code": "SENDING_RESUME_BLOCKED",
"error": "Sending is still under review.",
"title": "Sending could not be resumed",
"docsUrl": "https://docs.sequenzy.com/guides/sender-health",
"details": { "reason": "waiting_for_ai_review" }
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# List Audience Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/audience-enrollments
GET /api/v1/sequences/{sequenceId}/audience-enrollments
List and poll background audience enrollment runs
Recent audience enrollment runs for a sequence, newest first. Poll a single run with `GET /api/v1/sequences/{sequenceId}/audience-enrollments/{runId}` while its `status` is `queued` or `running`.
## Request
Sequence ID.
Runs to return, 1 to 100. Defaults to 20.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/audience-enrollments?limit=5" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Poll one run:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/audience-enrollments/aer_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Each run reports `status` (`queued`, `running`, `completed`, `cancelled`, `failed`), `estimatedCount` (audience size when the run started; `0` for incremental `auto_enroll` runs, which only scan recent joiners), `processedCount` (subscribers scanned so far), `enrolledCount`, `skippedCount` (matching contacts already in the sequence), `error`, `scheduledFor` (set while a queued run waits for its start time), `data` (the run-level object passed when the run started, read by its emails as `{{enrollment.}}`, or `null`), `source` (`api`, `dashboard`, `cli`, or `auto_enroll` for runs the keep-enrolling sync starts), and the `createdAt`, `startedAt`, `completedAt`, and `cancelRequestedAt` timestamps.
## Responses
```json 200 theme={null}
{
"success": true,
"audienceEnrollments": [
{
"id": "aer_abc123",
"sequenceId": "seq_abc123",
"status": "running",
"audience": { "type": "all" },
"data": null,
"targetNodeId": "node_delay_1",
"estimatedCount": 12000,
"processedCount": 4000,
"enrolledCount": 3800,
"skippedCount": 200,
"source": "dashboard",
"error": null,
"createdAt": "2026-11-01T10:00:00.000Z",
"startedAt": "2026-11-01T10:00:02.000Z",
"completedAt": null,
"cancelRequestedAt": null
}
]
}
```
```json 200 (single run) theme={null}
{
"success": true,
"audienceEnrollment": {
"id": "aer_abc123",
"sequenceId": "seq_abc123",
"status": "completed",
"audience": { "type": "all" },
"data": null,
"targetNodeId": "node_delay_1",
"estimatedCount": 12000,
"processedCount": 12400,
"enrolledCount": 11800,
"skippedCount": 200,
"source": "dashboard",
"error": null,
"createdAt": "2026-11-01T10:00:00.000Z",
"startedAt": "2026-11-01T10:00:02.000Z",
"completedAt": "2026-11-01T10:03:40.000Z",
"cancelRequestedAt": null
}
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Audience enrollment not found"
}
```
# Cancel Audience Enrollment
Source: https://docs.sequenzy.com/api-reference/sequences/cancel-audience-enrollment
POST /api/v1/sequences/{sequenceId}/audience-enrollments/{runId}/cancel
Stop a queued or running audience enrollment
Stop an audience enrollment run started with [Enroll Audience](/api-reference/sequences/enroll-audience). A queued run is cancelled immediately. A running run stops after the batch it is currently enrolling, so `status` stays `running` with `cancelRequestedAt` set until the worker observes the request.
Contacts already enrolled stay in the sequence. Use [Cancel Enrollments](/api-reference/sequences/cancel-enrollments) to remove them.
## Request
Sequence ID.
Audience enrollment run ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/audience-enrollments/aer_abc123/cancel" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"audienceEnrollment": {
"id": "aer_abc123",
"sequenceId": "seq_abc123",
"status": "cancelled",
"audience": { "type": "all" },
"data": null,
"targetNodeId": "node_delay_1",
"estimatedCount": 12000,
"processedCount": 0,
"enrolledCount": 0,
"skippedCount": 0,
"source": "api",
"error": null,
"createdAt": "2026-11-01T10:00:00.000Z",
"startedAt": null,
"completedAt": "2026-11-01T10:00:20.000Z",
"cancelRequestedAt": "2026-11-01T10:00:20.000Z"
},
"message": "Audience enrollment cancelled."
}
```
```json 400 theme={null}
{
"error": "This enrollment is already completed"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Audience enrollment not found"
}
```
# Cancel Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/cancel-enrollments
POST /api/v1/sequences/{sequenceId}/enrollments/cancel
Cancel active or waiting subscriber enrollments in one sequence
Cancel active or waiting enrollments in a single sequence. Provide exactly one target:
| Target | Use it when |
| - | - |
| `cancelAll` | You want to fully stop a live sequence, including segment-triggered enrollments that share no entry field value |
| `subscriberIds` | You have a specific batch of contacts to pull out |
| `subscriberId` | You are cancelling one contact |
| `fieldValues` | Enrollments are keyed on a stored entry event property such as `order.id` |
Pausing enrollment only stops new entrants. Cancelling with `cancelAll` is what removes the contacts already mid-flight.
## Request
Sequence ID.
Cancel every active or waiting enrollment in the sequence, regardless of how
contacts entered it. Defaults to a dry run unless you set `dryRun` to `false`.
Subscriber ID to cancel in this sequence.
Up to 500 subscriber IDs to cancel in this sequence. IDs that do not resolve
come back in `target.notFoundSubscriberIds`. Defaults to a dry run unless you
set `dryRun` to `false`.
Dot-path inside the token's entry event properties, such as `order.id` or
`event.id`. If omitted, the sequence's `enrollmentFieldPath` is used.
Entry field values to match.
When `true`, returns matching enrollments without cancelling them.
`cancelAll`, `subscriberIds`, and `fieldValues` default to a dry run unless
you set `dryRun` to `false`. A single `subscriberId` cancels immediately.
Optional reason stored on cancelled enrollment tokens.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments/cancel" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"cancelAll": true,
"dryRun": false,
"reason": "Lifecycle cutover"
}'
```
## Paging through a large cancellation
Cancelling an enrollment emits a `sequence.stopped` event per contact, so each request cancels at most 1,000 enrollments. When the response reports `remainingCount` above zero, send the same request again until it reaches zero.
```bash theme={null}
# The CLI does this loop for you.
sequenzy sequences cancel-enrollments seq_abc123 --all --apply --reason "Lifecycle cutover"
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"dryRun": false,
"target": { "mode": "all" },
"matchedCount": 3522,
"cancelledCount": 1000,
"remainingCount": 2522,
"enrollments": [
{
"tokenId": "tok_abc123",
"subscriberId": "sub_abc123",
"subscriberEmail": "customer@example.com",
"status": "cancelled",
"enrollmentKey": "segment:seg_abc123"
}
],
"hasMore": true,
"message": "Cancelled 1000 sequence enrollments. 2522 still match; repeat this request to continue."
}
```
```json 400 theme={null}
{
"error": "Provide exactly one target: subscriberId, subscriberIds, fieldValues, or cancelAll."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Check Sequence Step
Source: https://docs.sequenzy.com/api-reference/sequences/check
POST /api/v1/sequences/{sequenceId}/nodes/{nodeId}/check
Find broken links, placeholders and content problems in a sequence email step
Run the pre-send email check on one saved sequence email step: the same rules as the email checker in the editor, plus live verification of every link and image. Run it for each email step before you activate a sequence.
This endpoint is read-only. It requires the `sequences:read` scope and never
sends or enrolls anyone. It uses `POST` only so personalization input can
travel in a request body.
## Request
Sequence ID.
Email step node ID. Get it from [Get Sequence](/api-reference/sequences/get).
Check as this stored subscriber: picks their localization and reports merge
tags and conditions that would not resolve for them. Link and content rules
always run on the email as written. Use this or `subscriber`, not both.
Requires the `subscribers:read` scope as well.
Check as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Extra merge variables layered over the contact's attributes.
Check a specific localization instead of deriving it from the contact.
Check a specific A/B test variant. Required for steps whose `nodeType` is
`action_ab_test`, which have no email of their own. Also needs the
`ab_tests:read` scope.
Verify every link and image over the network. Pass `false` for a fast,
rules-only check that makes no requests.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/nodes/node_abc123/check" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
## What gets checked
Links and images are collected from every version recipients can receive: each A/B variant and each translation. Pass `variantId` or `locale` to check only that version.
**Links and images, live.** Every web link and image is requested from our servers (HEAD, then GET when a server refuses HEAD), following up to 8 redirects. Each one comes back with a `status`:
| Status | Meaning |
| - | - |
| `ok` | Answered with a success status, after any redirects. |
| `broken` | Page not found (404/410), the domain doesn't exist, an SSL certificate problem, a redirect loop, or a rejected request. Images that don't return an image also count. |
| `invalid` | Can never work for recipients: a rule already found it unusable (for example localhost, a private address, a mistyped `https://` or no `https://` at all), or the domain points to a private address. |
| `server_error` | The server answered with a 5xx error. Often temporary. |
| `unreachable` | No answer in time, or the connection was refused or reset. |
| `restricted` | The site refused an automated check (for example 401, 403 or 429), or redirects to itself to set a cookie. It may work fine in a browser, so it is never counted as broken. |
| `personalized` | Built from merge tags filled in per recipient, such as `{{event.orderUrl}}`. Only its syntax is checked. |
| `not_checked` | `mailto:` and `tel:` links, links not reached before the 25-second time limit, or `links` was `false`. |
`{{company.url}}` is resolved from your company website before checking. If no website is set, those links are reported as `links.missing-website`.
**Links, without a request.** Placeholder links (`#`, a bare `https://`), buttons with no link, script links, links missing `https://` or relative links, mistyped schemes such as `htps://`, unencoded spaces, incomplete domains, localhost and private addresses, tunnel and deploy-preview URLs, staging subdomains, placeholder domains such as `example.com`, `http://` links, link shorteners, raw IP addresses, link text that shows a different domain than the link goes to, invalid `mailto:`/`tel:` links, and unclosed merge tags in URLs.
**Images.** Broken or non-image URLs, images over 1 MB, embedded base64 images, local file paths, placeholder image services, and missing alt text.
**Content.** Leftover placeholder copy (lorem ipsum, `[First Name]`, TODO), name merge tags without a fallback, spam-trigger wording, excessive capitals, profanity, too many or no links, a missing unsubscribe link in marketing email, empty conditional branches, and broken merge tag syntax.
**Subject and preview text.** Length, placeholder text, a fake `Re:` or `Fwd:` prefix, spam-trigger wording, capitals, punctuation, emoji, and preview text that repeats the subject.
**Rendered HTML.** Gmail clipping risk, button contrast and tap-target size, and features specific email clients drop.
Sender settings (From and Reply-To) are not part of this check.
## Response
`true` when the check ran.
0 to 100 across subject (25%), preview text (15%) and content (40%), rescaled
to 100 because sender settings aren't part of this check. The editor also
weighs your sender (20%), so its score can differ slightly for the same email.
`A`, `B`, `C`, `D` or `F`.
Predicted inbox tab: `Primary`, `Promotions` or `Spam`.
`subject`, `preview` and `content`, each with `score` and `maxScore`.
Findings, errors first. Each has `category`, `severity` (`error` to fix before
sending, `warning` to review, `info` for tips) and `message`, plus a stable
`rule` ID such as `links.broken` or `content.placeholder-text`. When the
finding has one location, `blockId` names the block to edit and `url` the link
or image concerned.
Every link and image: `url`, `kind` (`link` or `image`), `label`, `blockId`,
`blockType`, `status`, `httpStatus`, `finalUrl` (after redirects), `message`,
and `findings` from the rules that need no request.
Counts: `total`, `checked`, `ok`, `broken` (broken and invalid), `warnings`
(server\_error and unreachable), `restricted` and `notChecked` (personalized
and not\_checked).
`false` when you passed `links: false`.
Subject line with merge tags resolved for the checked contact.
Preview text with merge tags resolved, or `null` when unset.
Localization the check used.
Merge tags that did not resolve, as on the [render
endpoint](/api-reference/sequences/render).
Block conditions this check could not decide, as on the [render
endpoint](/api-reference/sequences/render).
What was checked: `type`, `id` and `variantId`.
Results for the same URL are cached for up to 10 minutes (1 minute for failures), so repeating a check right after fixing a broken link can briefly return the old answer. Live checks are limited to 30 calls a minute per API key and company, and 60 a minute per API key across companies; past that you get `429`, and `links: false` still works.
```json 200 theme={null}
{
"success": true,
"score": 79,
"grade": "C",
"placement": "Promotions",
"categories": {
"subject": { "score": 100, "maxScore": 100 },
"preview": { "score": 80, "maxScore": 100 },
"content": { "score": 65, "maxScore": 100 }
},
"issues": [
{
"category": "content",
"severity": "error",
"rule": "links.broken",
"message": "Broken link: \"Shop the sale\" (acme.com/sale) - Page not found (404).",
"blockId": "block-hero",
"url": "https://acme.com/sale"
},
{
"category": "content",
"severity": "error",
"rule": "links.missing-scheme",
"message": "Missing https:// - email clients won't open this: \"new arrivals\" (www.acme.com/new).",
"blockId": "block-intro",
"url": "www.acme.com/new"
}
],
"links": [
{
"url": "https://acme.com/sale",
"kind": "link",
"label": "Shop the sale",
"blockId": "block-hero",
"blockType": "hero",
"status": "broken",
"httpStatus": 404,
"finalUrl": null,
"message": "Page not found (404)",
"findings": []
}
],
"linkSummary": {
"total": 6,
"checked": 4,
"ok": 3,
"broken": 1,
"warnings": 0,
"restricted": 0,
"notChecked": 2
},
"linksChecked": true,
"subject": "New arrivals picked for you",
"previewText": "Fresh styles and restocked favorites",
"locale": "en",
"unresolvedMergeTags": [],
"unevaluatedConditions": [],
"entity": { "type": "sequence_step", "id": "node_abc123", "variantId": null }
}
```
```json 400 theme={null}
{
"error": "Provide either subscriberId or subscriber, not both."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 429 theme={null}
{
"error": "Too many link checks. Try again in a minute, or pass links: false for a rules-only check."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Create Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/create
POST /api/v1/sequences
Create an automation sequence with AI-generated or explicit email/action steps
If you use an account key as a marketer, choose an existing sender and
Reply-To profile. A request that needs a new profile returns `400` before
creating profiles, labels, or campaign/sequence changes. This also applies to
email steps and inserted sequence branches. An owner or admin can add the
identity first; a step-level display-name override can still use an existing
profile.
Create an automation sequence that sends emails based on triggers. You can:
* Provide a `goal` to have AI generate the email content
* Provide `steps` with explicit email content and actions such as discount creation or Update Subscriber
* Omit both `goal` and `steps` to create the same blank trigger-to-completion draft the dashboard starts with
New sequences are created as drafts, so the response reports
`"effectiveStatus": "draft"` and nobody is enrolled until you call
[Enable Sequence](/api-reference/sequences/enable). See
[List Sequences](/api-reference/sequences/list) for the full run-state mapping.
## Request
Name for the sequence
Dashboard description for the sequence. For a seasonal sale sequence (for
example a Black Friday countdown) it doubles as the offer: generated emails
quote its offer sentences (discount, code, scope) verbatim, so keep internal
notes out of it.
Dashboard label names. Missing labels are created automatically.
Whether recipients can cancel this sequence from email preferences.
Up to 10 addresses blind-copied on every sequence email.
Trigger type: `contact_added`, `tag_added`, `segment_entered`,
`segment_exited`, `event_received`, `inbound_webhook`, `inactivity`,
`frequency`, or `manual`. `segment_exited` enrolls active contacts who stop
matching the segment; unsubscribing or bouncing does not count as leaving.
Defaults to `contact_added` when omitted. `manual` never enrolls anyone
automatically: pair it with `keyDates` and `waitUntilKeyDate` steps for a
one-time countdown, then [enroll the
audience](/api-reference/sequences/enroll-audience) after enabling the
sequence.
From address for every email in this sequence. Its domain must be configured
and verified. Mutually exclusive with `senderProfileId`.
Display name recipients see, e.g. `Brennon at TradeTally`. Selects the sender
identity of that name on `fromEmail`, creating it when the address has no
identity by that name; the mailbox's other display names, and everything
pinned to them, are untouched. Requires `fromEmail`; omit it when you send
`senderProfileId`, which already carries its own display name.
Existing sender profile ID. It already supplies both the From address and the
display name, so send it on its own and omit `fromEmail` and `fromName`. To
keep this profile under a different display name, set `fromName` on the email
steps instead, where it is a per-step override.
Reply-To address for every email in this sequence. Mutually exclusive with
`replyProfileId`.
Display name for the Reply-To address. Requires `replyTo`; omit it when you
send `replyProfileId`, which already carries its own display name. An address
carries one Reply-To name company-wide: if `replyTo` already has a saved
profile under a different name, that saved name is kept and the response
`warnings` array says so.
Existing reply profile ID. It already supplies both the Reply-To address and
the display name, so send it on its own and omit `replyTo` and `replyToName`.
List ID (for `contact_added` trigger). Omit it to use `listScope` instead. Use
`listIds` to trigger on several lists.
Several list IDs (for `contact_added` trigger). A contact joining **any** of
them enters the sequence. Takes precedence over `listId` when you send both.
Cannot be combined with `listScope`. Up to 25 lists.
For `contact_added` triggers with no list at all. `any_contact` enrolls every
contact that is added, including contacts that join no list - which is what an
integration creates when its list targeting is empty. `any_list` waits until
the contact joins a list. Cannot be combined with `listId` or `listIds`.
Tag name (for `tag_added` trigger). Use `tagNames` to trigger on several tags.
Several tag names (for `tag_added` trigger). Receiving **any** of them enters
the contact into the sequence. Takes precedence over `tagName` when you send
both. Up to 25 tags.
Segment ID (for `segment_entered` and `segment_exited` triggers)
For `segment_entered`, cancel an enrollment when the subscriber leaves the
segment.
Event name (for `event_received`, `inactivity`, `frequency` triggers)
Integration slug for an `inbound_webhook` trigger.
Integration event key for an `inbound_webhook` trigger.
Custom integration metadata for an `inbound_webhook` trigger.
Event property filters (for `event_received` and `inbound_webhook` triggers,
maximum 10).
The sequence only starts when the triggering event's properties match ALL
filters. Use a dot-path into the event properties; use `[]` to match items
inside arrays, for example `lineItems[].providerProductId`.
Dot-path into the event properties, such as `plan`,
`lineItems[].providerProductId`, or `productIds`.
One of `exists`, `not_exists`, `equals`, `not_equals`, `one_of`,
`contains`, `greater_than`, or `less_than`. `one_of` matches when the
property equals any entry of the value array - for example, scope a
trigger to several products with
`{"path": "lineItems[].providerProductId", "operator": "one_of", "value": ["prod_1", "prod_2"]}`.
Value to compare against. Required for every operator except `exists`
and `not_exists`. For `one_of`, pass a non-empty array of strings or
numbers with at most 50 values; all other operators take a single
value.
Days of inactivity (for `inactivity` trigger)
For `inactivity` triggers, when to start counting for subscribers who have
never performed the event: `sequence_created_at` or `subscriber_created_at`.
Defaults to `sequence_created_at`.
Minimum event count (for `frequency` trigger)
Time window in days (for `frequency` trigger)
Re-entry mode: `unlimited`, `one_time`, or `matching_field`. Defaults to
`matching_field` for Shopify replenishment and back-in-stock events, otherwise
`unlimited`. `matching_field` is only valid for `event_received` and
`inbound_webhook` sequence triggers.
Scalar dot-path event property used by `matching_field`, such as `order.id` or
`product.providerVariantId`. Array traversal with `[]` is not supported; use
`propertyFilters` when the trigger must match values inside an array. Leave
empty for the built-in Shopify product/variant default.
Optional local-time sending window for every email step in the sequence. Email
steps that become due outside the window wait until the next allowed local
time.
Set to `false` to disable the window. Omit `sendingWindow` when you do not
want a sending restriction.
IANA timezone for the window, such as `Europe/Kiev` or
`America/New_York`.
Earliest local send time in `HH:mm` format, for example `08:00`.
Latest local send cutoff in `HH:mm` format, for example `20:00`. Must be
later than `startTime`.
Allowed local days. Use full day names such as `monday`; omit to allow
every day.
Whether the sequence respects the company [frequency
cap](/api-reference/frequency-cap/get). When `true`, an email step is skipped
for a contact who already received the cap's number of campaign or marketing
sequence emails in its window, and the contact moves on to the next step.
Transactional steps are never skipped and transactional emails never count.
Has no effect until a cap is set. Non-boolean values return `422`.
Named absolute moments the sequence counts down to, such as "Black Friday
starts". Delay steps with `waitUntilKeyDate` wait relative to one of them, so
the whole countdown is re-timed by editing these dates. Set to `null` to
clear.
IANA timezone the dates are shown and edited in, such as
`America/New_York`.
Up to 20 entries of `{ key?, label, at }`. `at` is an ISO 8601
date-time; a value without a zone designator such as
`2026-11-27T00:00` is read as wall-clock time in `timezone`, and every
value is returned normalized to UTC. `key` (lowercase letters, digits,
underscores) defaults to a slug of the label, so "Black Friday starts"
becomes `black_friday_starts`. Keys must be unique.
Optional auto-stop condition, re-evaluated before every step including the
first one. Use `has_tag` to end the sequence when a subscriber gets a tag,
`event_received` when an event is tracked, `added_to_list` when a subscriber
joins a list, `entered_segment` when a subscriber enters a segment,
`field_changed` when a subscriber attribute changes, or `none` to clear
auto-stop behavior. `does_not_have_tag` and `removed_from_list` stop the run
whenever the subscriber lacks that tag or list membership, so they act as a
required-tag or required-list allowlist and cancel everyone else before any
step sends. Guarded-out contacts still enroll and are then cancelled at the
trigger node, so they appear as cancellations there rather than in the active
or waiting enrollment counts. Clearing the guard does not retry them: they
only receive the sequence if the trigger fires for them again, and on the
`one_time` enrollment mode not even then. `event_received` only counts events
received after enrollment; the enrolling event and earlier history do not
satisfy the stop. Use `matchConfig.mode: "event_property_filter"` with
`propertyFilters` to require every literal-value filter to match a later stop
event, or `matchConfig.mode: "event_property"` with field mapping rules to
compare the stop event with the enrolling event. `field_changed` can include
`matchConfig.mode: "field_value"` with an operator and comparison value. Tag
and list defaults are returned with `value: null` and `matchConfig.mode:
"entry_audience"`; sending that shape back preserves the per-enrollment tag or
list guard.
### Option A: AI Generation
Description of what the sequence should accomplish. AI will generate email
content based on this.
Number of emails to generate (default: `5`, maximum: `10`)
Total duration in days used to space AI-generated emails. Omit this to use the
default sequence delay schedule.
Style for the AI-generated emails: `visual` (designed, with heroes, imagery,
and rich sections) or `plain` (personal, text-first notes with a single
button). Defaults to the company's saved preference when omitted.
### Option B: Explicit Content
Array of explicit steps. Email steps must have either `blocks` or `html`; SMS steps use `type: "sms"` with `text`; push steps use `type: "push"` with `title` and/or `body`; discount steps use `type: "create_discount"`; Update Subscriber steps use `type: "update_subscriber"`, `nodeType: "action_update_attributes"`, and `config`.
Step type. Omit or use `email` for email content. Use `sms` for a native SMS step, `push` for a push notification, `create_discount` for a dynamic discount, or `update_subscriber` to copy event data onto the subscriber.
Use `action_update_attributes` for Update Subscriber steps.
Update Subscriber step config. `firstName`, `lastName`, and
`customAttributeUpdates[].value` can use standalone merge tags such as
`{{event.plan}}`, `{{event.amount}}`, and `{{event.active}}`. Set
`valueType` to `text`, `number`, or `boolean`; number and boolean values
are coerced after resolution.
Email subject line. Required for email steps.
SMS steps only: plain-text message body. Merge tags like `{{FIRST_NAME}}` work. Do not include opt-out text or a brand prefix - Sequenzy adds both automatically at send time. Generate drafts with the [Generate SMS endpoint](/api-reference/generate/sms).
SMS steps only: up to 2 publicly reachable image URLs sent as MMS media.
Push steps only: notification title, up to 120 characters. Merge tags work.
Push steps only: notification message, up to 500 characters. Merge tags work.
Push steps only: link opened on tap, an https URL or an app deep link.
Push steps only: optional https image.
Push steps only: optional https web push icon.
Push steps only: limit delivery to `web`, `ios`, and/or `android`. Omit for every platform.
SMS and push steps: `skip` (default) continues the sequence when the contact can't receive the message (SMS: no phone, not opted in, unsupported country; push: no active device); `exit` removes them from the sequence.
SMS and push steps: display label for the step in the builder.
Email preview text
Use transactional chrome for this sequence email.
Existing sender profile override for this email step. It may be combined
with `fromName`, which changes only the visible display name for this
step. Mutually exclusive with `fromEmail`.
From address for this email step. Its domain must be configured and
verified. Mutually exclusive with `senderProfileId`.
Display name override for this email step. Alone or with
`senderProfileId`, it changes only the visible name; with `fromEmail`, it
also names a newly created sender profile.
Existing Reply-To profile override for this email step. Mutually
exclusive with `replyTo`.
Reply-To address for this email step. Mutually exclusive with
`replyProfileId`.
Display name for this step's Reply-To address. Requires `replyTo`; omit
it when using `replyProfileId`, which already carries its own display
name.
Per-step visible copy recipients.
Per-step blind-copy recipients.
URL-backed file attachments for this email step, as `{ filename, path }`
objects. `path` can be a public HTTPS URL or, for an event-triggered
sequence, an event value such as `{{event.file_url}}`. `filename` can
also contain event merge tags, for example
`file-{{event.order_id}}.pdf`. Values are resolved from the event that
enrolled the subscriber, then the final URL is validated and fetched at
send time (max 10 attachments, 15MB total per email). Base64 content is
not supported here.
Email content blocks in Sequenzy format.
Provide `blocks` OR `html`, not both. Put block styling under `styles`;
top-level style keys like `backgroundColor`, `backgroundOpacity`,
`textColor`, `textAlign`, `borderColor`, `borderWidth`, and
`borderRadius` are normalized into `styles`.
Blocks are validated against the block schema. A block that is structurally
invalid is rejected with `400` naming the step, the block index, and the
field, rather than stored - a stored block the renderer cannot render fails
every enrollment at that step permanently. Fields that parse but will not
render as their name suggests come back in an advisory `warnings` array on
the `200`.
Raw HTML content (plain HTML, React Email output, MJML output, etc.).
Stored as one raw HTML block; Sequenzy does not convert it to native
blocks.
Provide `html` OR `blocks`, not both.
Fixed delay before this email. To wait until a date from the trigger
event or a contact attribute, set `mode` to `until_date` and provide
`untilDateField`, or use the `waitUntil` shorthand.
`duration` for fixed waits or `until_date` for dynamic date-field
waits. Defaults to `duration`.
Days to waitHours to waitMinutes to wait
Date field path to wait until when `mode` is `until_date`, for
example `renews_at`.
`event` (default) reads `untilDateField` from the trigger event;
`attribute` reads it from the contact's custom attributes.
`before` or `after`. Defaults to `after`.
`continue` or `exit` when the date field is missing or invalid.
Defaults to `continue`.
`continue`, `skip`, or `exit` when the date already passed. Same
behavior as `waitUntil.pastAction`. Defaults to `continue` for
`until_date`.
Shorthand for a dynamic delay that waits until a date from the trigger
event or a contact attribute. The value must be an ISO 8601 date, such
as `2027-02-15`, or a Unix timestamp. For a per-contact timeline that
contacts can join at any point, such as emails before and after a due
date, read the date from an attribute and set `pastAction` to `skip`.
Date field path, for example `renews_at`, `subscription.renews_at`,
or `dueDate`.
Where `field` is read from. `event` (default) reads the trigger
event's properties and needs an event trigger. `attribute` reads the
contact's custom attributes when the step is reached, so it works
with any trigger. A contact already waiting here is held longer if
the date moves later. Moving it earlier does not release them sooner,
and a date removed while they wait follows `missingAction`.
Optional offset object with `days`, `hours`, and `minutes`.
`before` or `after`. Defaults to `after`.
`continue` or `exit` when the date field is missing or invalid.
Defaults to `continue`.
What a contact does when they reach this step after the date (with
its offset) already passed: `continue` (default) moves on
immediately, `skip` skips the following email and action steps until
the next wait, condition or branch so late joiners only receive what
is still ahead, `exit` ends the
enrollment. Use `continue` on a final wait that every contact should
reach, for example a last email after the date.
Hold this step until the next occurrence of a weekday inside a
local-time window, for example next Sunday between 09:00 and 12:00 in
`America/Los_Angeles`. Contacts reaching the step inside the window
continue immediately; everyone else waits for the next occurrence. Place
it immediately before the email when the send day matters: it controls
when the flow resumes, so any step between it and the email shifts the
send off the window. A sequence-level `sendingWindow` holds emails at
send time but does not reschedule the graph.
Single weekday, for example `sunday` or `sun`. Shorthand for `days`.
Weekday names the wait may release on. The nearest upcoming day wins.
Window start in 24-hour `HH:mm` local time, for example `09:00`.
Window end in 24-hour `HH:mm` local time. Defaults to the end-of-day
boundary `24:00`. A
wake-up missed past `endTime` (rare operational failure) rolls to the
next configured day instead of releasing late.
IANA timezone the window is evaluated in, for example
`America/Los_Angeles`.
Wait relative to one of the sequence's `keyDates` before this step, for
example 7 days before `black_friday_starts` or 1 hour before
`black_friday_ends`. Provide only one of `waitUntil`, `waitUntilWeekday`,
or `waitUntilKeyDate`.
Key date identifier from `keyDates`.
Offset days.Offset hours.Offset minutes.
`before` or `after` the key date. Defaults to `before`. Leave the
offset at zero to fire exactly at the key date.
What a contact does when they reach this step after the moment
already passed: `skip` (default) skips the following steps until the
next wait so late enrollees only receive what is still ahead,
`continue` sends immediately, `exit` ends the enrollment.
`exit` (default) or `continue` when the key date no longer exists.
Delay before this step in milliseconds. Prefer `delay` for hand-written
requests; use `delayMs` when importing provider waits.
Optional name for the email template.
Discount configuration for `create_discount` steps. For new integrations, put discount options here; legacy top-level discount fields are still accepted.
Builder label for discount steps.
Discount provider. Use `stripe` to dynamically create a Stripe coupon plus promotion code, or `shopify` to dynamically create a Shopify Admin discount code.
Discount type: `percent` or `amount`.
Percent discount from `1` to `100`. Required when `discountType` is `percent`.
Fixed discount in the smallest currency unit, for example `500` for `$5`. Required when `discountType` is `amount`.
Currency for fixed-amount discounts. Defaults to `usd`.
Coupon duration: `once`, `forever`, or `repeating`. Defaults to `once`.
Required for `repeating` discounts.
Whether the discount applies to all plans. Defaults to `true`.
Provider product IDs when `appliesToAllPlans` is `false`. Stripe uses IDs like `prod_abc123`; Shopify accepts numeric product IDs or `gid://shopify/Product/...` IDs.
Optional prefix for generated dynamic codes. The final code also includes a subscriber/token suffix.
Maximum redemptions for each generated code. Use `1` for subscriber-specific codes.
Stripe-only. When `true`, each generated promotion code is restricted to the matched subscriber's Stripe customer.
Optional future expiration date or ISO timestamp. Mutually exclusive with `expiresInHours`.
Optional relative expiration in hours, resolved to an absolute timestamp when each subscriber's code is created. Use this for flows like "valid for 48 hours" that should restart per enrollment. Takes precedence over `expiresAt` when both are provided.
Optional display name for each dynamically generated provider discount.
### Option C: Blank Draft
Send only `name` (and optionally metadata or a trigger). The API creates a
disabled draft containing the trigger and completion nodes, ready for later
`insertSteps`, branch insertion, or graph edits.
## Request Shape and Warnings
Every trigger setting is a top-level field. A `contact_added` sequence for one
list uses `"trigger": "contact_added"` and `"listId"`, not a nested
`triggerConfig` object. Step delays are objects such as `"delay": { "days": 2 }`,
or `delayMs`.
This minimal request creates a six-email draft for one list, sending the first
email right away and each later email two days after the previous one:
```json theme={null}
{
"name": "Welcome series",
"trigger": "contact_added",
"listId": "list_abc123",
"enrollmentMode": "one_time",
"steps": [
{ "subject": "Welcome", "html": "
", "delay": { "days": 2 } }
]
}
```
Omit `stopCondition` to keep the default list guard, which stops a run when the
contact leaves the list. Send `{ "type": "none" }` to turn it off.
For compatibility, some requests that do not match this shape still create the
draft. The response then includes a `warnings` array that names each problem:
| Sent | Result |
| - | - |
| `triggerConfig`, `triggerType` or another unknown field | Ignored. Send trigger settings as top-level fields. |
| `delay: 2`, `delay: "2d"`, `delayDays` or `delayHours` | Ignored, so the step has no wait. Use `delay: { "days": 2 }` or `delayMs`. |
| A `contact_added` list ID not found in your company | Saved, but enabling is rejected until the trigger uses an existing list. |
| A body `companyId` that differs from the selected company | Ignored. Select the company with the `X-Company-Id` header. |
| `contact_added` with no `listId`, `listIds` or `listScope` | Enrolls every contact added to the company once enabled. Enable repeats this warning. |
Fix every warning before you [enable the sequence](/api-reference/sequences/enable).
Send `"listScope": "any_contact"` when you do want every new contact. Enable
Sequence still notes that audience in its `warnings`, so you can confirm it
before contacts start entering.
A field with the wrong type or an unsupported value, such as
`"enrollmentMode": "once"` or a `stopCondition` sent as a string, returns `422`.
The response names the field in `property` and explains it in `summary`, with
`allowedValues` for fields that accept a fixed set of values.
## Discount Merge Tags
When a `create_discount` step runs, Sequenzy creates a fresh provider discount/code for that subscriber's automation run. Later emails in the same sequence can use discount merge tags from the most recent created discount:
* `{{discount.code}}`
* `{{discount.percentOff}}`
* `{{discount.amountOff}}`
* `{{discount.currency}}`
* `{{discount.expiresAt}}`
Discount sequences require a connected integration for the selected provider, such as Stripe or Shopify, before activation.
## Enrollment Modes
Use `matching_field` for event-triggered sequences where each run belongs to a specific object in the event payload. It lets a subscriber have separate active runs for different products, variants, orders, subscriptions, or other event-scoped objects, while blocking duplicate active runs for the same resolved field. Set `enrollmentFieldPath` for custom events, or send `sequenceEnrollmentKey`, `enrollmentKey`, or common product/variant fields in the event properties.
```json theme={null}
{
"trigger": "event_received",
"eventName": "ecommerce.order_placed",
"enrollmentMode": "matching_field",
"enrollmentFieldPath": "order.id"
}
```
## Sending Windows
Use `sendingWindow` when a triggered sequence should start immediately but emails
should only go out during allowed local hours. The window is checked before every
email action, including later steps after delays or branches.
```json theme={null}
{
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
}
}
```
## Event-Personalized Sequence Emails
If you create a sequence with `trigger: "event_received"`, the event payload that enrolls the subscriber is available inside sequence emails through `event.` merge tags.
For example, if your app sends:
```json theme={null}
{
"email": "user@example.com",
"event": "weather.wind_alert",
"properties": {
"city": "Tel Aviv",
"alert": {
"maxSpeed": 75
},
"duration": {
"hours": 6
}
}
}
```
You can use values like:
* `{{event.city}}`
* `{{event.alert.maxSpeed}}`
* `{{event.duration.hours|a few}}`
Sequenzy stores the event snapshot on the sequence run, so delayed follow-up emails in the same sequence keep using the original values.
## Examples
### AI-Generated Content
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Onboarding Sequence",
"trigger": "contact_added",
"stopCondition": {
"type": "has_tag",
"value": "customer"
},
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"goal": "Welcome new users and help them get started with our product",
"emailCount": 5,
"durationDays": 14
}'
```
### Explicit Content with Blocks
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Sequence",
"trigger": "contact_added",
"steps": [
{
"subject": "Welcome to our product!",
"blocks": [
{
"id": "heading-1",
"type": "heading",
"content": "Welcome!",
"level": 1
},
{
"id": "text-1",
"type": "text",
"content": "
Thanks for signing up. We are excited to have you.
",
"variant": "paragraph"
}
]
}
]
}'
```
### Explicit Content with HTML (Auto-converted)
Use this when migrating from other platforms or when you have existing HTML templates (React Email, MJML, etc.).
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Migrated Sequence",
"trigger": "contact_added",
"steps": [
{
"subject": "Welcome!",
"html": "
Welcome to our platform!
We are thrilled to have you on board.
Get Started"
},
{
"subject": "Quick tips to get started",
"delay": { "days": 2 },
"html": "
Getting Started Guide
Here are some tips:
Complete your profile
Explore the dashboard
Connect your integrations
"
}
]
}'
```
### Explicit Discount Action
Use this when you want Sequenzy to generate a dynamic provider discount code before sending a follow-up email. Connect the selected provider in Settings before enabling the sequence.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Win-back Discount",
"trigger": "tag_added",
"tagName": "cancelled",
"steps": [
{
"type": "create_discount",
"discount": {
"label": "Create win-back discount",
"provider": "stripe",
"discountType": "percent",
"percentOff": 20,
"duration": "once",
"appliesToAllPlans": true,
"maxRedemptions": 1,
"lockToSubscriber": true,
"codePrefix": "WINBACK"
}
},
{
"subject": "Come back with {{discount.code}}",
"delay": { "days": 1 },
"html": "
Use {{discount.code}} for {{discount.percentOff}}% off your next subscription.
"
}
]
}'
```
### Inactivity Sequence
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Login Win-back",
"trigger": "inactivity",
"eventName": "app.login",
"inactiveDays": 14,
"inactivityBaseline": "sequence_created_at",
"goal": "Bring inactive users back into the app"
}'
```
### Product-Scoped Purchase Sequence (Property Filters)
Use `propertyFilters` to start a purchase sequence only for one specific product, for example to deliver a digital product file. Use `lineItems[].providerProductId` for `ecommerce.order_placed` events, or `productIds` for Stripe `saas.purchase` events.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Ebook Delivery",
"trigger": "event_received",
"eventName": "saas.purchase",
"propertyFilters": [
{
"path": "productIds",
"operator": "equals",
"value": "prod_ABC123"
}
],
"steps": [
{
"subject": "Your download is ready",
"html": "
The original alert is still active for {{event.city}}. Check local guidance if conditions change.
"
}
]
}'
```
## Responses
```json 200 AI Generation theme={null}
{
"success": true,
"message": "Sequence created. AI is generating email content based on your brand. This typically takes 30-60 seconds.",
"sequence": {
"id": "seq_abc123",
"name": "Onboarding Sequence",
"status": "draft",
"enrollmentPaused": false,
"effectiveStatus": "draft",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": false,
"effectiveStatusSummary": "Draft. Nobody is enrolled and no emails are sent.",
"emailCount": 5,
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
}
}
}
```
```json 200 Explicit Content theme={null}
{
"success": true,
"message": "Sequence created with 2 emails and 1 discount action spanning 1 day. Use POST /api/v1/sequences/{id}/enable to activate.",
"sequence": {
"id": "seq_abc123",
"name": "Welcome Sequence",
"status": "draft",
"enrollmentPaused": false,
"effectiveStatus": "draft",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": false,
"effectiveStatusSummary": "Draft. Nobody is enrolled and no emails are sent.",
"emailCount": 2,
"discountCount": 1,
"nodeCount": 6,
"sendingWindow": null
}
}
```
```json 200 Custom Event With Matching-Field Enrollment theme={null}
{
"success": true,
"message": "Sequence created with 1 email. Use POST /api/v1/sequences/{id}/enable to activate. Add the custom event feed using eventTrackingCode and eventTracking before enabling.",
"sequence": {
"id": "seq_abc123",
"name": "Trial Started",
"status": "draft",
"enrollmentPaused": false,
"effectiveStatus": "draft",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": false,
"effectiveStatusSummary": "Draft. Nobody is enrolled and no emails are sent.",
"emailCount": 1,
"nodeCount": 3
},
"eventTrackingCode": "await fetch(\"https://api.sequenzy.com/api/v1/subscribers/events\", { ... properties: { trial_id: \"\" } ... });",
"eventTracking": {
"endpoint": "https://api.sequenzy.com/api/v1/subscribers/events",
"method": "POST",
"docsUrl": "https://docs.sequenzy.com/api-reference/subscribers/events/trigger",
"integrationGuide": {
"tool": "get_integration_guide",
"arguments": { "use_case": "event_tracking" }
},
"payloadContract": {
"required": ["event", "properties"],
"identity": "Provide email or externalId. Email is required when the subscriber does not already exist.",
"event": "Must equal \"trial.started\".",
"requiredPropertyPaths": ["trial_id"]
},
"examplePayload": {
"email": "user@example.com",
"event": "trial.started",
"properties": { "trial_id": "" }
},
"examplePayloadMatchesFilters": true
},
"requiredEvents": ["trial.started"]
}
```
```json 200 With Warnings theme={null}
{
"success": true,
"message": "Sequence created with 6 emails. Use POST /api/v1/sequences/{id}/enable to activate.",
"sequence": {
"id": "seq_abc123",
"name": "Welcome series",
"status": "draft",
"enrollmentPaused": false,
"effectiveStatus": "draft",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": false,
"effectiveStatusSummary": "Draft. Nobody is enrolled and no emails are sent.",
"emailCount": 6,
"sendingWindow": null
},
"warnings": [
"Ignored unknown field \"triggerConfig\". Send trigger settings as top-level fields such as \"trigger\", \"listId\", \"listIds\", \"tagName\", \"segmentId\" and \"eventName\".",
"Ignored \"steps[1].delay\": expected an object, so step 2 was saved without this delay. Use \"delay\": { \"days\": 2 } (days, hours, minutes) or \"delayMs\".",
"This contact_added sequence has no list and no \"listScope\", so once enabled it enrolls every contact added to this company. Send \"listId\" to limit it to one list, or \"listScope\": \"any_contact\" to keep this behavior."
]
}
```
```json 400 theme={null}
{
"error": "Either 'goal' (for AI generation) or 'steps' (for explicit content) is required"
}
```
```json 400 theme={null}
{
"error": "Step 1: either 'blocks' or 'html' is required"
}
```
```json 400 theme={null}
{
"error": "Step 1: invalid block at 0.url: Invalid input: expected string, received undefined"
}
```
```json 400 theme={null}
{
"error": "Step 1: expiresAt must be in the future"
}
```
```json 400 theme={null}
{
"error": "Invalid trigger type 'invalid'. Valid types: contact_added, tag_added, segment_entered, segment_exited, event_received, inbound_webhook, inactivity, frequency, manual"
}
```
```json 400 theme={null}
{
"error": "propertyFilters is only supported for the event_received trigger"
}
```
```json 400 theme={null}
{
"error": "propertyFilters[0].value is required for the 'equals' operator"
}
```
```json 400 theme={null}
{
"error": "emailCount must be no more than 10"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/enrollmentMode",
"message": "Expected union value",
"summary": "Property 'enrollmentMode' must be one of: unlimited, one_time, matching_field",
"found": { "name": "Welcome series", "enrollmentMode": "once" },
"errors": [
{
"path": "/enrollmentMode",
"message": "Expected union value",
"summary": "Property 'enrollmentMode' must be one of: unlimited, one_time, matching_field",
"allowedValues": ["unlimited", "one_time", "matching_field"]
}
]
}
```
## Notes
* The sequence is created in `draft` status. Use `POST /api/v1/sequences/{id}/enable` to activate it.
* For backwards compatibility, `POST /v1/sequences` is still accepted as an alias.
* When using email `steps`, each email step creates an email template in your account that can be edited later.
* Sequences with `create_discount` steps require a connected integration for the selected provider before activation.
* SMS steps require the SMS add-on (paid plans). Check [Get SMS Settings](/api-reference/sms/settings) first; steps added while SMS is not ready are stored but skip at runtime.
* Push steps send only once web push, APNs or Firebase is set up. Check [Get Push Settings](../push/settings) first; see [Push notifications](../../concepts/push-notifications).
* When using `goal`, AI generates content asynchronously. Poll `GET /api/v1/sequences/{id}` to check enrichment status.
* The first step typically has no delay (sends immediately after trigger).
* If `sendingWindow` is set, each email step sends only during the allowed local
hours and waits until the next allowed time otherwise.
* For `event_received` sequences, properties sent to `POST /api/v1/subscribers/events` are available as `{{event.*}}` merge tags in sequence emails.
* For custom event triggers, use the returned `eventTrackingCode` and
`eventTracking` contract to implement the application-owned event feed. The
contract includes normalized `propertyFilters` and reports whether its
generated example already satisfies them through
`examplePayloadMatchesFilters`. If that value is `false`, adapt the example
using `payloadContract` and `examplePayloadNote` before sending it. With
`matching_field` enrollment, every required property path must be present in
`properties` or the sequence cannot identify the matching run.
* When using `html` in steps, the content is preserved as one raw HTML block.
Use `blocks` when you want native Sequenzy block editing.
# Create Sequence from Example
Source: https://docs.sequenzy.com/api-reference/sequences/create-from-example
POST /api/v1/sequences/from-example
Clone an email gallery sequence into a draft sequence written for your brand
Clone a public sequence from the [Sequenzy email gallery](https://sequenzy.com/email-examples/sequences) into a draft sequence for your company, the same as **Clone this sequence** in the dashboard.
The draft keeps the example's trigger family and send timing, up to 12 emails: the first 12, or the ones you choose with `stepNumbers`. AI then writes every email in your brand in the background, usually within 30 to 60 seconds. Poll [Get Sequence](/api-reference/sequences/get) until `enrichmentStatus` is `complete`; it reports `pending` until the first email is written, then `in_progress`. The sequence sends nothing until you [enable it](/api-reference/sequences/enable).
Every successful call creates another sequence, so do not retry after a success. Requires the `sequences:write` scope.
## Triggers and timing
| Example sequence | Starts when | Stops early |
| - | - | - |
| Onboarding | A contact is added (`contact_added`) | No |
| Abandoned cart | `ecommerce.cart_abandoned` is received | On `ecommerce.order_placed` |
| Post-purchase | `ecommerce.order_placed` is received | No |
| Re-engagement | The `inactive` tag is added | When the tag is removed |
Waits follow the real gaps between the example's emails, and every email you clone keeps its original timing from the trigger, even when you leave out the ones before it: gaps under a day stay in hours, with at least one hour between emails, and longer gaps round to whole days. Onboarding waits for the day the first email arrived after signup. The other types start at their trigger: cart recovery waits one hour before its first email, and post-purchase and re-engagement send their first email right away. You can change the trigger and waits in the builder before enabling.
## Request
Identify the example with `url`, or with `brand` and `sequence`. Sending both returns 400.
Gallery sequence page URL, such as
`https://sequenzy.com/email-examples/brands/linear/sequences/onboarding`. An
email URL returns 400.
Gallery brand slug. Use with `sequence`.
Gallery sequence slug, from `/sequences/{sequence}`. Use with `brand`.
Optional direction applied to every email, up to 1000 characters. It takes
priority over the example.
Sequence name, up to 200 characters. Defaults to the sequence type, such as
"Onboarding sequence".
Which of the example's emails to clone, as 1-based positions in the gallery
sequence (its Email 1, Email 2 and so on). These are positions in the example,
not the `stepNumber` of the created sequence's steps. Duplicates are ignored
and order does not matter. Defaults to the first 12. An empty list, more than
12 distinct positions, or a position outside the sequence returns 400. More
than 100 entries, or entries that are not integers, fail validation with 422.
`visual` or `plain`. Defaults to your company's email style preference.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/from-example" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand": "linear",
"sequence": "onboarding",
"brief": "Push new users to invite a teammate",
"stepNumbers": [1, 3]
}'
```
## Response
The draft: `id`, `name`, `status` (`draft`), `trigger`, `triggerDescription`,
`eventName` or `tagName` when the trigger uses one, `stopCondition` (the exit
rule the sequence was created with, when it has one), `emailCount`,
`enrichmentStatus` (`processing`, or `not_queued` when writing could not
start) and `steps`. Each step has `stepNumber`, `title`, `delayMs` (the wait
before it) and `originalDay`.
The gallery sequence: `brand`, `name`, `url`, `omittedEmailCount` (the
example's emails that were not cloned, because you left them out of
`stepNumbers` or they are beyond the 12-step limit) and `stepNumbers` (the
positions of the cloned emails in the example, ascending).
```json 200 theme={null}
{
"success": true,
"sequence": {
"id": "seq_3kLm9",
"name": "Onboarding sequence",
"status": "draft",
"trigger": "trigger_list",
"triggerDescription": "When someone signs up",
"emailCount": 2,
"enrichmentStatus": "processing",
"steps": [
{ "stepNumber": 1, "title": "Welcome", "delayMs": 0, "originalDay": 0 },
{ "stepNumber": 2, "title": "Feature tip", "delayMs": 432000000, "originalDay": 5 }
]
},
"example": {
"brand": "Linear",
"name": "Linear onboarding",
"url": "https://sequenzy.com/email-examples/brands/linear/sequences/onboarding",
"omittedEmailCount": 1,
"stepNumbers": [1, 3]
},
"message": "Draft sequence created with the example's trigger and timing. AI is writing each email in your brand; this typically takes 30-60 seconds. It stays a draft until you activate it."
}
```
```json 400 theme={null}
{
"error": "That url is an email example. Remix single emails with the template endpoint, or pass a sequence page url."
}
```
```json 400 (stepNumbers) theme={null}
{
"error": "Email numbers must be from 1 to 3; 5 is not one."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "That gallery sequence was not found. It may have been removed.",
"code": "EXAMPLE_NOT_FOUND"
}
```
```json 500 theme={null}
{
"error": "Failed to remix the example"
}
```
A 404 with code `EMAIL_GALLERY_UNAVAILABLE` means the email gallery is not available yet. A 422 means a field failed validation, such as a `brief` over 1000 characters or an unknown `emailStyle`.
If the draft is created but writing its emails cannot start, the response still succeeds, with `enrichmentStatus: "not_queued"` and a `warnings` entry. Open the sequence in the dashboard to write the emails instead of calling this endpoint again, which would create a second sequence.
# Delete Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/delete
DELETE /api/v1/sequences/{sequenceId}
Delete a sequence and its automation nodes
Deletes a sequence. This removes the automation and its nodes.
## Request
Sequence ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Sequence deleted",
"sequenceId": "seq_abc123"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Disable Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/disable
POST /api/v1/sequences/{sequenceId}/disable
Pause a sequence and hold workflow execution
Pauses a sequence, blocks new enrollments, and holds workflow execution.
Existing subscribers stay at their current step until the sequence is enabled
again.
## Request
Sequence ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/disable" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Sequence disabled",
"sequenceId": "seq_abc123",
"status": "paused",
"enrollmentPaused": true,
"effectiveStatus": "paused",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": false,
"effectiveStatusSummary": "Paused. Existing recipients are frozen in place and nobody new enters."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Enable Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/enable
POST /api/v1/sequences/{sequenceId}/enable
Activate a sequence so matching subscribers can enter it
Activates a draft or paused sequence and opens it for new enrollments. When a
paused sequence is enabled again, held subscribers continue from their current
step. Waits that expired during the pause resume as due work and are queued
gradually.
Activation runs the same readiness checks returned by
[`GET /sequences/{sequenceId}/simulate`](/api-reference/sequences/simulate).
Fix every readiness error before enabling; invalid triggers, incomplete steps,
and disconnected graphs are rejected without changing the sequence status.
Sequences that include `create_discount` steps require a connected integration for each selected discount provider before activation.
Readiness warnings do not block activation. They come back in a `warnings`
array on the `200`, for example when a `contact_added` trigger has no list and
so enrolls every contact added to your company. Check them, and
[update the trigger](/api-reference/sequences/update) or
[disable the sequence](/api-reference/sequences/disable) if the audience is not
what you intended.
## Request
Sequence ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enable" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Sequence enabled",
"sequenceId": "seq_abc123",
"status": "active",
"enrollmentPaused": false,
"effectiveStatus": "live",
"acceptsNewEnrollments": true,
"processesExistingEnrollments": true,
"effectiveStatusSummary": "Live. New subscribers enroll and existing recipients keep receiving steps."
}
```
```json 200 With Warnings theme={null}
{
"success": true,
"message": "Sequence enabled",
"sequenceId": "seq_abc123",
"status": "active",
"enrollmentPaused": false,
"effectiveStatus": "live",
"acceptsNewEnrollments": true,
"processesExistingEnrollments": true,
"effectiveStatusSummary": "Live. New subscribers enroll and existing recipients keep receiving steps.",
"warnings": [
"The trigger has no list, so every contact added to this company enters the sequence, including contacts that join no list. Choose a list on the trigger if that is not intended."
]
}
```
```json 400 theme={null}
{
"error": "Sequence is not ready to activate: No actions: Add at least one action step like an email, A/B test, discount, subscriber update, or list update"
}
```
```json 400 theme={null}
{
"error": "Connect Stripe before activating a sequence with discount steps"
}
```
```json 400 theme={null}
{
"error": "Connect Shopify before activating a sequence with discount steps"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Enroll Subscribers
Source: https://docs.sequenzy.com/api-reference/sequences/enroll
POST /api/v1/sequences/{sequenceId}/enroll
Manually enroll subscribers into a sequence
Manually enroll active subscribers into a sequence by email or subscriber ID. Enrollment starts at the first step after the trigger, or at a specific node when you pass `targetNodeId`. Subscribers that are inactive or already enrolled are skipped. You can pass up to 500 targets per request, combined across `emails` and `subscriberIds`.
The sequence must be open for enrollment: enabled (or capturing entrants) and not enrollment-paused.
## Request
Sequence ID.
Subscriber emails to enroll. Emails are trimmed and lowercased. Unknown emails
are returned in `notFound`.
Subscriber IDs to enroll.
Node to start enrollment at. Defaults to the first non-trigger node connected
to the trigger. Cannot be a trigger node. When the target is a delay step,
enrollment is scheduled after the delay.
Provide at least one of `emails` or `subscriberIds`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enroll" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails": ["user@example.com", "unknown@example.com"]}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"enrolled": 1,
"skipped": 0,
"notFound": ["unknown@example.com"],
"targetNodeId": "node_email_1",
"scheduledFor": "2026-06-11T10:00:00.000Z"
}
```
```json 400 theme={null}
{
"error": "Provide emails or subscriberIds"
}
```
```json 400 theme={null}
{
"error": "A maximum of 500 subscribers can be enrolled per request"
}
```
```json 400 theme={null}
{
"error": "Cannot enroll subscribers while sequence enrollment is closed"
}
```
```json 400 theme={null}
{
"error": "Target node not found in this sequence"
}
```
```json 400 theme={null}
{
"error": "Cannot enroll subscribers at the trigger node"
}
```
```json 400 theme={null}
{
"error": "Sequence has no enrollable step"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Enroll Audience
Source: https://docs.sequenzy.com/api-reference/sequences/enroll-audience
POST /api/v1/sequences/{sequenceId}/enroll-audience
Enroll everyone, lists, or a segment in a sequence as a background run
Enroll a whole audience in a sequence. Unlike [Enroll Subscribers](/api-reference/sequences/enroll), there is no 500-contact cap: Sequenzy starts a background run that walks the audience in batches and reports progress. This is how a manual-trigger countdown sequence, such as a Black Friday countdown, gets its audience: enable the sequence, then enroll the audience.
* Only active contacts with an email address are enrolled. Unsubscribed and bounced contacts are never enrolled.
* Contacts already active or waiting in the sequence are skipped. Sequences with `enrollmentMode: "one_time"` also skip contacts who completed or were cancelled before.
* Late enrollees in a sequence with key-date waits skip the steps whose moment already passed and continue from the next wait (the step's `pastAction`).
* Only one run per sequence can be queued or running at a time.
* The sequence must be enabled and accepting entrants.
Use `POST /api/v1/sequences/{sequenceId}/enroll-audience/estimate` with the same `audience` body to preview the counts without starting a run.
## Request
Sequence ID.
Who to enroll. Same shape as campaign `targetLists`.
`all`, `lists`, `segment`, `filtered`, or `rules`.
For `lists`: members of any of these lists.
For `segment`: members of this saved segment.
For `filtered`: subscriber filters, with an optional `filterJoinOperator` of `and` or `or`.
For `rules`: include rules of type `all`, `lists`, `segments`, or `filtered`.
For `rules`: exclude rules of type `lists`, `segments`, or `filtered`.
Step to start contacts at. Defaults to the first step after the trigger.
Cannot be a trigger node.
ISO 8601 date-time to start the run later, up to one year ahead. The run is
created with `status: "queued"` and `scheduledFor` set, and can be cancelled
before it starts. A past value starts now.
Run-level data shared by everyone in this run, such as a sale end time, a
discount percentage or an order cut-off time. Sequenzy stores it on the run and copies
it into every enrolled contact's sequence context at enrollment time, so the
sequence's emails and conditions read it as `{{enrollment.}}`
(nested paths such as `{{enrollment.draw.date}}` and `{{#each enrollment.prizes}}`
work). Must be a JSON object of at most 8 KB, 100 top-level keys and 8
levels of nesting; keys named `__proto__`, `constructor` or `prototype` and
strings containing NUL characters are rejected. Omit, `null` or `{}` for none. Contacts enrolled by a later run
see that run's data; contacts still active from an earlier run are skipped
and keep the earlier run's data. Contacts enrolled by an auto-enroll sync
(`audienceAutoEnroll`) have no run data. Stored as JSONB, so key order is
not preserved and numbers are normalized (`1.0` reads back as `1`).
To keep enrolling people who join the audience after this run (new list
members, new segment matches) until the countdown ends, set
`audienceAutoEnroll` on the sequence with [Update Sequence](/api-reference/sequences/update).
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enroll-audience" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audience": {"type": "segment", "segmentId": "seg_past_buyers"}}'
```
With run-level data for a flash sale (every email in the run reads
`{{enrollment.sale_ends_at}}` and `{{enrollment.discount_percent}}`):
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enroll-audience" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audience": {"type": "segment", "segmentId": "seg_past_buyers"},
"data": {
"sale_ends_at": "2026-11-29T23:59:00Z",
"discount_percent": 20,
"order_cutoff_utc": "2026-11-29T22:00:00Z",
"shop_url": "https://example.com/sale"
}
}'
```
Estimate first:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enroll-audience/estimate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audience": {"type": "all"}}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"audienceEnrollment": {
"id": "aer_abc123",
"sequenceId": "seq_abc123",
"status": "queued",
"audience": { "type": "segment", "segmentId": "seg_past_buyers" },
"data": null,
"targetNodeId": "node_delay_1",
"estimatedCount": 11800,
"processedCount": 0,
"enrolledCount": 0,
"skippedCount": 0,
"source": "api",
"error": null,
"createdAt": "2026-11-01T10:00:00.000Z",
"scheduledFor": null,
"startedAt": null,
"completedAt": null,
"cancelRequestedAt": null
},
"message": "Enrolling about 11,800 contacts in the background. Poll GET /sequences/seq_abc123/audience-enrollments/aer_abc123 for progress."
}
```
```json 200 (estimate) theme={null}
{
"success": true,
"matchingCount": 12000,
"alreadyEnrolledCount": 200,
"enrollableCount": 11800
}
```
```json 400 theme={null}
{
"error": "Activate the sequence before enrolling an audience."
}
```
```json 400 theme={null}
{
"error": "The target step does not belong to this sequence"
}
```
```json 400 theme={null}
{
"error": "data must be a JSON object such as {\"discount_percent\": 20}"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
```json 409 theme={null}
{
"success": false,
"code": "run_in_progress",
"error": "An audience enrollment is already in progress for this sequence. Wait for it to finish or cancel it first."
}
```
## Failure and retry semantics
The run is idempotent per contact: the cursor is committed together with every batch of enrollments, so a retried worker resumes where it stopped and never enrolls the same contact twice. Every contact the run enrolls, in any batch or after a retry, receives the same copy of `data`. There is no way to change a run's `data` afterwards; start a new run for new values. If the run fails after retries, its `status` becomes `failed` with an `error`, and the contacts enrolled before the failure stay in the sequence. Cancelling stops after the current batch and keeps everyone already enrolled.
# Get Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/get
GET /api/v1/sequences/{sequenceId}
Get sequence metadata, graph topology, and editable email steps
Returns one sequence with its automation nodes, edges, graph revision, and
editable email steps. Pass `graphRevision` back with `graphEdit` when updating
the topology so a stale client cannot overwrite a newer graph change. Each
linked email includes its effective `emailPreset` (`branded` or `minimal`), the
same value shown under **Style > Format** in the dashboard. Native block emails
can include supported custom HTML blocks and still return their format. An email
stored entirely as one standalone raw HTML block returns `null`. Each email
step also returns `emailTheme`, the theme override stored on that one email;
`null` means the step has no override and renders on the company theme.
`emails` covers every email-sending step, including steps converted to an A/B
test. Those return `nodeType: "action_ab_test"` plus an `abTest` object with the
test id, status, and one entry per variant. Their step-level `subject`,
`previewText`, and `blocks` are **control variant A only**. With `ab_tests:read`,
each `abTest.variants[]` entry includes that variant's full `blocks`, so a
sequence read is enough to audit competing copy. `get_ab_test` still returns the
full test record (settings, localization, stats). Change variant copy with
[Update Variant](/api-reference/ab-tests/update-variant). That read-and-update
workflow needs `ab_tests:read`, `ab_tests:write`, and `sequences:write`.
`emailCount` counts these steps too, and
`abTestCount` reports how many of them are A/B steps.
Keys with `sequences:read` but without `ab_tests:read` still receive the A/B
step and its control copy. The nested `abTest` summary keeps the configured test
id and editing guidance, but redacts the test record and competing variants;
its `name` and `status` are `null`, and `variants` is empty.
Each linked email includes both `stepNumber`, the legacy stored ordinal retained
for backward compatibility, and `structuralStepNumber`, its graph-derived
position. Email nodes expose the same read-only value at
`nodes[].structuralStepNumber`, outside their editable config. Use the structural
value for UI labels and sequence analysis. Emails in parallel branches
intentionally share the same structural depth; after unequal branches merge,
depth continues from the longer incoming path.
Read `effectiveStatus` (`draft`, `live`, `enrollment_paused`, `paused`, or
`archived`) to know whether the sequence is running. `status` alone still reads
`active` while new enrollments are paused. See
[List Sequences](/api-reference/sequences/list) for the full mapping, and note
that `triggerConfig.active` is a legacy field the runtime ignores - it mirrors
`acceptsNewEnrollments`.
## Request
Sequence ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx"
```
`keyDates` lists the countdown moments a manual sequence counts down to, and
`audienceAutoEnroll` the "keep enrolling later joiners" setting with its
read-only `enabledAt`, `lastSyncedAt`, `endedAt` and `endedReason` fields; both
are `null` for ordinary sequences. See [Update Sequence](/api-reference/sequences/update)
for their shapes.
## Responses
```json 200 theme={null}
{
"success": true,
"sequence": {
"id": "seq_abc123",
"name": "Welcome Sequence",
"status": "active",
"enrollmentPaused": false,
"effectiveStatus": "live",
"acceptsNewEnrollments": true,
"processesExistingEnrollments": true,
"effectiveStatusSummary": "Live. New subscribers enroll and existing recipients keep receiving steps.",
"trigger": "trigger_list",
"senderProfileId": "sender_abc123",
"fromName": "Acme",
"fromEmail": "hello@example.com",
"replyProfileId": "reply_abc123",
"replyToName": "Support",
"replyToEmail": "support@example.com",
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"frequencyCapEnabled": true,
"keyDates": null,
"audienceAutoEnroll": null,
"graphRevision": "48ab6d78a2d4f2b7d2d36d8e1dba9ea3f411bb65059b627a2b8268b4e81d8f7a",
"nodes": [
{
"id": "node_trigger",
"nodeType": "trigger_list",
"config": { "triggerType": "contact_added" }
},
{
"id": "node_abc123",
"nodeType": "action_email",
"structuralStepNumber": 1,
"config": {
"emailId": "email_abc123",
"stepNumber": 1,
"emailPreset": "minimal",
"emailTheme": { "presetId": "soft", "colors": { "background": "#ffffff" } }
}
},
{
"id": "node_end",
"nodeType": "action_webhook",
"config": { "isEndNode": true }
}
],
"edges": [
{
"sourceNodeId": "node_trigger",
"targetNodeId": "node_abc123"
},
{
"sourceNodeId": "node_abc123",
"targetNodeId": "node_end"
}
],
"emails": [
{
"nodeId": "node_abc123",
"emailId": "email_abc123",
"stepNumber": 1,
"structuralStepNumber": 1,
"name": "Welcome Sequence - Email 1",
"subject": "Welcome to Acme",
"previewText": null,
"emailPreset": "minimal",
"emailTheme": { "presetId": "soft", "colors": { "background": "#ffffff" } },
"delayNodeId": null,
"delayMode": null,
"delayMs": null,
"waitUntil": null,
"waitUntilWeekday": null,
"delayDisplay": null,
"delayDescription": null,
"blocks": [],
"nodeType": "action_email",
"abTest": null
},
{
"nodeId": "node_def456",
"nodeType": "action_ab_test",
"emailId": "email_variant_a",
"stepNumber": 2,
"name": "Welcome Sequence - Email 2 Variant A",
"subject": "Still thinking it over?",
"previewText": null,
"emailPreset": "minimal",
"emailTheme": null,
"blocks": [],
"abTest": {
"id": "ab_abc123",
"name": "A/B Test - Follow-up",
"status": "testing",
"testType": "content",
"winnerCriteria": "click_rate",
"winnerThreshold": 100,
"winningVariantId": null,
"variantCount": 2,
"variants": [
{
"variantId": "var_a",
"variantLabel": "A",
"emailId": "email_variant_a",
"subject": "Still thinking it over?",
"previewText": null,
"isWinner": false,
"blockCount": 1,
"blocks": [
{
"id": "control",
"type": "text",
"content": "
"
}
]
}
],
"contentEditing": {
"readTool": "get_ab_test",
"writeTool": "update_ab_test_variant",
"requiredScopes": ["ab_tests:read", "ab_tests:write", "sequences:write"],
"note": "This step's copy lives on the A/B test variants..."
}
}
}
],
"createdAt": "2026-04-20T10:00:00.000Z",
"enrichmentStatus": "complete",
"emailCount": 2,
"abTestCount": 1,
"enrichedCount": 2
}
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Get Sequence Enrollment
Source: https://docs.sequenzy.com/api-reference/sequences/get-enrollment
GET /api/v1/sequences/{sequenceId}/enrollments/{enrollmentId}
Read one enrollment token, including how it entered and which branches it already took
Read a single enrollment in a sequence: how it entered, the redacted branch
decisions it has already taken, and the recorded graph walk from ClickHouse.
Use this when [list enrollments](/api-reference/sequences/list-enrollments)
shows a completed token with `enteredVia` unknown or sitting on the completion
node and you need to know **why** the first branch took its else path. Listing
does not return ClickHouse history; this endpoint does.
## Request
Sequence ID.
Enrollment token ID from [list
enrollments](/api-reference/sequences/list-enrollments) (`enrollmentId`).
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments/tok_abc123" \
-H "Authorization: Bearer seq_live_xxx"
```
## Response fields
`enrollment` is the same row shape as [list
enrollments](/api-reference/sequences/list-enrollments), including `enteredVia`,
`entryContext`, and `branchDecisions`.
* `entryContext` is event or manual-enrollment context captured at entry:
trigger type, event id, event name, and **property keys only**. Values from
the enrolling payload are never returned.
* `branchDecisions` is the redacted if/else (or random-split) verdict this token
already took within the retained window, persisted on the token going
forward. `branchDecisionCount` is the total and
`branchDecisionsTruncated` says whether older decisions were omitted. Compared values are
summaries (`missing`, `empty`, `nonempty`, `equals_expected`), never the raw
field or event-property value.
* `nodeHistory` is the ClickHouse graph walk for this token, oldest first. Each
step may include a `branchDecision` reconstructed from stored node-completion
metadata. Legacy rows that stored an unredacted evaluation reason are
redacted on read.
* `nodeHistoryTruncated` says whether more events exist beyond the returned
`nodeHistoryLimit` events. Do not treat a truncated graph walk as complete.
* `historySource` is `node_events`, `token_context`, `both`, or `none`.
```json theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"sequenceName": "New Producer Activation",
"historySource": "both",
"enrollment": {
"enrollmentId": "tok_abc123",
"status": "completed",
"enteredVia": {
"kind": "event",
"value": "producer.activation.needed",
"name": null,
"description": "Event \"producer.activation.needed\""
},
"entryContext": {
"triggerType": "event_received",
"eventId": "evt_abc123",
"eventName": "producer.activation.needed",
"hasEventProperties": true,
"eventPropertyKeys": ["catalogueStage"],
"hasFieldSnapshots": false,
"fieldSnapshotKeys": []
},
"branchDecisions": [
{
"nodeId": "node_branch",
"decidedAt": "2026-08-22T00:00:01.234Z",
"splitMode": "condition",
"selectedPath": "else",
"matchedBranchId": null,
"matchedBranchIndex": -1,
"routedEdgeBranchId": "else",
"evaluations": [
{
"branchId": "branch-stage",
"branchIndex": 0,
"conditionType": "field_equals",
"fieldName": "catalogueStage",
"outcome": "fail",
"compared": {
"present": false,
"summary": "missing",
"kind": null
},
"reason": "Field catalogueStage not found on subscriber"
}
]
}
],
"branchDecisionCount": 1,
"branchDecisionsTruncated": false
},
"nodeHistory": [
{
"nodeId": "node_branch",
"nodeType": "logic_branch",
"nodeLabel": "catalogueStage",
"eventType": "completed",
"eventTime": "2026-08-22T00:00:01.234Z",
"branchDecision": {
"selectedPath": "else"
}
}
],
"nodeHistoryTruncated": false,
"nodeHistoryLimit": 200
}
```
# Sequence Goals
Source: https://docs.sequenzy.com/api-reference/sequences/goals
Create and manage persisted conversion goals for a sequence
Sequence goals are the persisted conversion signals shown in the dashboard.
They are separate from the `goal` prompt used to generate sequence copy.
## Endpoints
* `GET /api/v1/sequences/{sequenceId}/goals` lists goals.
* `POST /api/v1/sequences/{sequenceId}/goals` creates a goal.
* `PATCH /api/v1/sequences/{sequenceId}/goals/{goalId}` updates a goal.
* `DELETE /api/v1/sequences/{sequenceId}/goals/{goalId}` deletes a goal.
Event goals use `triggerType: "event"` and require `triggerEventName`.
Subscriber-attribute goals use `triggerType: "attribute_change"`, an
`attributePath`, and `attributeCondition` set to `changed`, `changed_to`, or
`changed_from_to`. The latter conditions also require the corresponding target
and previous values. Tag goals use `triggerType: "tag_added"` and require
`triggerTagName`.
```json theme={null}
{
"name": "Account reactivated",
"triggerType": "event",
"triggerEventName": "account.reactivated",
"attributionWindowHours": 168
}
```
For event goals, `eventPropertyName` and `eventPropertyLabel` can track a
numeric property such as revenue. `attributionWindowHours` accepts whole hours
from 1 to 720. Both create and update accept `isActive`, so you can create a
goal disabled or pause tracking later without deleting the goal.
Sequence goal names cannot duplicate a company-wide goal or a goal on another
sequence in the same company. Creating or renaming a sequence goal to an
existing name returns `409 Conflict`.
The CLI exposes the same operations under `sequenzy sequences goals`, and MCP
clients can use `list_sequence_goals`, `create_sequence_goal`,
`update_sequence_goal`, and `delete_sequence_goal`.
# Sequence Inbound Webhook
Source: https://docs.sequenzy.com/api-reference/sequences/inbound-webhook
Configure the endpoint attached to an inbound-webhook sequence trigger
First create or update the sequence with `trigger: "inbound_webhook"` and an
`eventName`. Then configure its secret endpoint:
* `GET /api/v1/sequences/{sequenceId}/inbound-webhook` returns its URL, mapping,
sample payload, and setup status.
* `PUT /api/v1/sequences/{sequenceId}/inbound-webhook` creates or updates the
endpoint.
* `POST /api/v1/sequences/{sequenceId}/inbound-webhook/rotate-secret` replaces
the secret and immediately invalidates the previous URL.
On first setup, omitted `PUT` fields apply the defaults from the sequence's
selected catalog or custom integration. On later calls, omitted fields keep
their saved values, so a captured sample payload survives a mapping-only
update. A custom mapping requires an `email` path and can optionally map names
and event properties.
```json theme={null}
{
"fieldMapping": {
"email": "payload.contact.email",
"firstName": "payload.contact.first_name",
"properties": {
"reply": "payload.reply",
"summitId": "payload.summit.id"
}
},
"samplePayload": {
"payload": {
"contact": { "email": "person@example.com" },
"reply": "Too expensive"
}
}
}
```
Set `fieldMapping` or `samplePayload` to `null` to clear it. The endpoint is
`active` once it has a mapping; otherwise it remains `pending_setup`.
The CLI exposes these operations as `sequenzy sequences webhook get`,
`configure`, and `rotate`. MCP clients can use
`get_sequence_inbound_webhook`, `configure_sequence_inbound_webhook`, and
`rotate_sequence_inbound_webhook_secret`.
# List Sequences
Source: https://docs.sequenzy.com/api-reference/sequences/list
GET /api/v1/sequences
List automation sequences in your current company
Returns the sequences for the company attached to your API key.
## Reading the run state
Do not read `status` literally. A sequence with `"status": "active"` and
`"enrollmentPaused": true` still emails the subscribers already inside it but
takes nobody new. Branch on `effectiveStatus` instead:
| `effectiveStatus` | New subscribers enroll | Existing recipients advance |
| - | - | - |
| `draft` | No | No |
| `live` | Yes | Yes |
| `enrollment_paused` | No | Yes |
| `paused` | No | No |
| `archived` | No | No |
The same two answers are also returned directly as `acceptsNewEnrollments` and
`processesExistingEnrollments`, plus `effectiveStatusSummary`, a plain-language
sentence you can show a user verbatim.
`triggerConfig.active` is a legacy field that the runtime does not read. It is
reported as a mirror of `acceptsNewEnrollments` so it can no longer contradict
the sequence state.
## Request
Stored status: `draft`, `active`, `paused`, or `archived`. An `active`
sequence may have enrollments paused; read `effectiveStatus` for that
distinction.
Case-insensitive match on sequence name or description.
Comma-separated dashboard label names. Matches any selected label. Unknown
names do not create labels; if no name matches, the result is empty.
Alias for `labels`. If both are provided, `label` takes precedence.
Page size, from 1 to 100. Providing `limit` or `offset` enables pagination. If
only `offset` is provided, the page size is 50. Omit both to return every
matching sequence.
Number of matching sequences to skip. Must be zero or greater.
The response always includes `pagination` with `limit` (or `null` when
unpaginated), `offset`, `count`, `total` and `hasMore`. Results are ordered by
creation date, newest first.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences?limit=20&offset=0" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequences": [
{
"id": "seq_abc123",
"name": "Welcome Sequence",
"status": "active",
"enrollmentPaused": true,
"effectiveStatus": "enrollment_paused",
"acceptsNewEnrollments": false,
"processesExistingEnrollments": true,
"effectiveStatusSummary": "Live for existing recipients. New enrollments are paused, so nobody new enters.",
"trigger": "trigger_list",
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"frequencyCapEnabled": true,
"createdAt": "2026-04-20T10:00:00.000Z"
}
],
"pagination": { "limit": 20, "offset": 0, "count": 1, "total": 1, "hasMore": false }
}
```
```json 400 theme={null}
{
"error": "Invalid status filter. Valid values: draft, active, paused, archived"
}
```
Invalid `limit`, `offset`, or label syntax also returns `400`.
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
# Duplicate and Archive Sequences
Source: https://docs.sequenzy.com/api-reference/sequences/list-actions
Manage the sequence-level actions available in the dashboard list
The public API exposes the same sequence-list actions as the dashboard.
## Duplicate
`POST /api/v1/sequences/{sequenceId}/duplicate` creates an independent draft.
The graph, linked email templates, and sequence A/B tests are copied; metrics,
winner state, and in-progress generation state are reset. The request body is
optional and accepts `name`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_123/duplicate" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{"name":"Welcome Series v2"}'
```
The copy is returned as `sequence`, reporting
`"effectiveStatus": "draft"` until you call
[Enable Sequence](/api-reference/sequences/enable). See
[List Sequences](/api-reference/sequences/list) for the full run-state mapping.
## Archive
`POST /api/v1/sequences/{sequenceId}/archive` moves a sequence to the dashboard
archive and stops new enrollments.
`POST /api/v1/sequences/{sequenceId}/unarchive` restores an archived sequence
as a disabled draft so it can be reviewed before activation.
Both endpoints return `{ "success": true, "sequence": ... }`.
# List Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/list-enrollments
GET /api/v1/sequences/{sequenceId}/enrollments
List the individual contacts currently enrolled in a sequence
List the contacts enrolled in one sequence, with the node each one is currently sitting on. Use this when [sequence stats](/api-reference/sequences/stats) gives you `enrollmentCounts` and you need the actual subscribers behind a number - for example, everyone waiting at one step.
By default the response includes only `active` and `waiting` enrollments, which is every contact still moving through the sequence.
## Request
Sequence ID.
Comma-separated enrollment statuses to include: `active`, `waiting`,
`completed`, `failed`, or `cancelled`. Defaults to `active,waiting`.
Comma-separated sequence node IDs. Only enrollments currently sitting on one
of these nodes are returned. Node IDs come from [Get
Sequence](/api-reference/sequences/get) or from
`enrollmentCounts.byCurrentNode` in [sequence
stats](/api-reference/sequences/stats).
Comma-separated subscriber IDs. Use this to check where specific contacts
stand.
Exact email address to match, case-insensitive.
Result order: `enrolled_at_desc` (default), `enrolled_at_asc`,
`wait_until_asc`, or `wait_until_desc`. Use `wait_until_asc` to see which
contacts resume next; enrollments with no scheduled resume sort last.
Annotate each returned `active` or `waiting` enrollment with whether the
sequence's stop condition already matches for that contact right now. Defaults
to `false`. Caps the page at 100 regardless of `limit`. See [Confirming a stop
condition](#confirming-a-stop-condition).
Enrollments per page, 1-500. Defaults to 50. Values above 500 are capped, and
above 100 when `stopConditionMatch` is `true`.
Number of enrollments to skip. Page until `pagination.hasMore` is `false`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments?currentNodeId=node_wave_1&status=waiting&sort=wait_until_asc&limit=500" \
-H "Authorization: Bearer seq_live_xxx"
```
## Exporting the full list
Page with `limit` and `offset` until `pagination.hasMore` is `false`. The CLI does this loop for you and writes a CSV:
```bash theme={null}
sequenzy sequences enrollments seq_abc123 --node-id node_wave_1 --all --csv enrollments.csv
```
CSV exports flatten the entry source into `enteredViaKind`, `enteredViaValue`
(the raw list/segment ID, tag, or event), and `enteredViaName` (the resolved
list/segment display name when available).
## Response fields
Each enrollment is one run through the sequence, identified by `enrollmentId`.
* `waitUntil` is when a waiting enrollment is scheduled to resume, or `null` when nothing is scheduled.
* `lastUpdatedAt` is the last change to the enrollment. For a waiting enrollment this is when it arrived at its current node; the platform does not separately track node entry time.
* `currentNodeMissing` is `true` when the node an enrollment sits on has been deleted from the sequence graph. `currentNodeType` and `currentNodeLabel` are omitted in that case.
* `email` falls back to the address captured at enrollment when the subscriber record no longer exists.
* `failedReason` explains why a `failed` enrollment stopped, and is `null` for every other status. Failures are terminal - the enrollment is not retried - so this is the only record of what went wrong.
* `movedFromNodeId`, `movedAt`, and `moveReason` record a release performed by [move enrollments](/api-reference/sequences/move-enrollments). They are `null` for an enrollment that reached its current step on its own, so they are how you tell a released wave apart from organic arrivals.
* `stopCondition` echoes the sequence's single configured stop condition and is always returned. Its `matchConfig` contains event-property filters, field comparisons, or `entry_audience` for a tag/list default; in that last case `value` is `null` because each enrollment resolves its own entry tag or list. `stopConditionMatches` and `stopConditionMatchReason` are only filled in when you pass `stopConditionMatch=true`.
* `enteredVia` records what put the contact into the sequence: `kind` is one of `list`, `tag`, `segment`, `event`, `inactivity`, `frequency`, `manual`, `test_run`, or `unknown`, and `value` carries the list ID, tag name, segment ID, or monitored event name behind it. For a list or segment, `name` resolves the ID for display and is `null` when the referenced resource has since been deleted. `inactivity` and `frequency` distinguish time-based evaluation from an ordinary received-event trigger. This is how you tell entrants apart when a trigger covers [several lists or tags](/concepts/sequences). `manual` means a dashboard or API enrollment that bypassed the trigger; `unknown` covers enrollments recorded before this field existed.
* `entryContext` is the event or manual-enrollment context captured at entry: `triggerType`, `eventId`, `eventName`, and **property keys only**. Payload values are never returned.
* `branchDecisions` is the bounded retained if/else (or random split) history. `branchDecisionCount` is the total and `branchDecisionsTruncated` says whether older decisions were omitted. Compared values are summaries (`missing`, `empty`, `nonempty`, `equals_expected`), never the raw field value. Empty on enrollments recorded before this field existed; use [get enrollment](/api-reference/sequences/get-enrollment) to reconstruct those from ClickHouse.
For the ClickHouse graph walk of **one** token, including legacy branch
metadata that was never copied onto the token, call [get
enrollment](/api-reference/sequences/get-enrollment).
## Confirming a stop condition
A stop condition does not cancel a waiting enrollment at the moment its event
arrives. It is re-evaluated when the enrollment next runs a step, so an
enrollment parked on a three-day delay keeps reporting `waiting` for the full
three days even though its stop event already landed. When the delay expires the
enrollment is cancelled before the next step runs, so nothing sends - but until
then `status` alone cannot tell you whether the stop registered.
Pass `stopConditionMatch=true` to run the same evaluation the worker will run:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments?subscriberId=sub_abc123&stopConditionMatch=true" \
-H "Authorization: Bearer API_KEY"
```
`stopConditionMatches: true` means the condition matches at the time of this
read. `null` means the match was not determined - the flag was not passed, the
sequence has no stop condition, the enrollment is no longer `active` or
`waiting`, or it fell outside the evaluated window. It never means "does not
match"; `stopConditionMatchEvaluatedCount` tells you how many rows were actually
checked.
This is a non-atomic reading of the current state, not a cancellation lock or a
promise of a future exit. The condition can change again before the enrollment
runs, a paused sequence may not run at its scheduled time, and a step that
already passed its stop check may still finish. The worker stays authoritative.
If you need the enrollment to *read* as cancelled immediately rather than at
its next step, call [cancel
enrollments](/api-reference/sequences/cancel-enrollments) directly.
## Diagnosing a failing step
Filter to failures and group them by `currentNodeId`:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments?status=failed" \
-H "Authorization: Bearer API_KEY"
```
The same `failedReason` on the same `currentNodeId` across several enrollments points at that step's configuration or content, not at the contacts. `GET /v1/sequences/{sequenceId}/stats` reports the same reasons per step as `steps[].failedCount` and `steps[].failedSubscribers[]`.
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"sequenceName": "Onboarding",
"statuses": ["waiting"],
"stopCondition": {
"type": "event_received",
"value": "onboarding.completed",
"matchConfig": null
},
"stopConditionMatchEvaluatedCount": 1,
"enrollments": [
{
"enrollmentId": "tok_abc123",
"sequenceId": "seq_abc123",
"subscriberId": "sub_abc123",
"email": "customer@example.com",
"firstName": "Wanda",
"lastName": "Waiter",
"subscriberStatus": "active",
"status": "waiting",
"currentNodeId": "node_wave_1",
"currentNodeType": "logic_delay",
"currentNodeLabel": "Start at Wave 1",
"currentNodeMissing": false,
"enrollmentKey": "__default__",
"enrollmentStartedAt": "2026-01-01T00:00:00.000Z",
"waitUntil": "2026-01-04T00:00:00.000Z",
"lastUpdatedAt": "2026-01-01T00:00:00.000Z",
"failedReason": null,
"movedFromNodeId": null,
"movedAt": null,
"moveReason": null,
"stopConditionMatches": true,
"stopConditionMatchReason": "Subscriber received stop event: onboarding.completed",
"enteredVia": {
"kind": "list",
"value": "list_abc123",
"name": "Newsletter",
"description": "Added to list \"Newsletter\""
},
"entryContext": {
"triggerType": "contact_added",
"eventId": null,
"eventName": "contact.added",
"hasEventProperties": false,
"eventPropertyKeys": [],
"hasFieldSnapshots": false,
"fieldSnapshotKeys": []
},
"branchDecisions": [],
"branchDecisionCount": 0,
"branchDecisionsTruncated": false
}
],
"pagination": {
"limit": 500,
"offset": 0,
"count": 1,
"total": 1,
"hasMore": false
}
}
```
```json 400 theme={null}
{
"error": "Invalid status filter `paused`. Valid values: active, waiting, completed, failed, cancelled"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Move Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/move-enrollments
POST /api/v1/sequences/{sequenceId}/enrollments/move
Release a bounded batch of enrolled contacts from one sequence step onto another
Release a bounded batch of contacts off one sequence step and onto another, keeping their enrollment intact. Use this when you want the next N contacts waiting on a delay to continue early: pace a launch, drain a hold step, or push one wave past a wait without touching the graph.
You could approximate this by cancelling the enrollments and re-enrolling them at the target step, but that throws away the enrollment's entry event properties, its stop-condition entry snapshots, and its original start date, and it is refused outright while the sequence has new enrollment paused. Moving does none of those things.
Moved contacts become `active` on the target step right away, so the sequence emails them as soon as a worker picks them up. Every bulk target defaults to a dry run: pass `dryRun: false` to actually move them.
## Request
Sequence ID.
Node ID the contacts are currently sitting on. Read it from `currentNodeId` on
[list enrollments](/api-reference/sequences/list-enrollments).
Node ID to move them onto. Defaults to the source step's only next step, and
is required when that step branches or is terminal. Cannot be the trigger
node.
Maximum enrollments to move in this call. Defaults to `100`, maximum `500`.
Which enrollments to take first: `wait_until_asc` (default),
`wait_until_desc`, `enrolled_at_asc`, or `enrolled_at_desc`. The default takes
the contacts that have been waiting longest for their next step.
Optional narrowing filter: only move these subscribers, up to 500.
Guardrail. Refuses to move more than this many enrollments onto `targetNodeId`
in a rolling 24 hours, counting the moves recorded by earlier calls. The
response reports `movedInWindow` and `dailyRemaining` so a paced release can
resume the next day.
Tag names applied to the moved contacts so the released wave stays
identifiable. The tags must already exist, and this requires the
`subscribers:tag` scope. Applying them never enrolls contacts in `tag_added`
sequences.
Note stored on every moved enrollment and returned as `moveReason` by [list
enrollments](/api-reference/sequences/list-enrollments).
When `true` (the default), reports which enrollments would move without moving
them. Pass `false` to apply.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments/move" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"fromNodeId": "node_delay_2",
"targetNodeId": "node_email_3",
"limit": 180,
"dailyLimit": 500,
"tags": ["wave-3-released"],
"reason": "Wave 3",
"dryRun": false
}'
```
## Pacing a release
Each call is bounded on purpose, so it is never drained automatically. When the response reports `remainingCount` above zero, send the same request again to release the next batch. Combine `limit` with `dailyLimit` to spread a large backlog over several days without tracking the running total yourself.
Active enrollments, enrollments a worker is currently mid-step on, and enrollments parked awaiting double opt-in are never moved. Active tokens may still have older queue work, so only safely parked waiting tokens can be repositioned. Excluded enrollments are reported in `skippedCount` and omitted from `matchedCount`.
```bash theme={null}
# The CLI is deliberately one batch per invocation too.
sequenzy sequences move-enrollments seq_abc123 --from-node node_delay_2 --limit 180 --apply
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"dryRun": false,
"fromNodeId": "node_delay_2",
"targetNodeId": "node_email_3",
"sort": "wait_until_asc",
"requestedLimit": 180,
"effectiveLimit": 180,
"matchedCount": 4320,
"movedCount": 180,
"remainingCount": 4140,
"skippedCount": 0,
"dailyLimit": 500,
"movedInWindow": 0,
"dailyRemaining": 320,
"enqueuedCount": 180,
"enqueueErrors": [],
"tagResult": {
"tags": ["wave-3-released"],
"updated": 180,
"unchanged": 0,
"failed": 0,
"failures": []
},
"enrollments": [
{
"tokenId": "tok_abc123",
"subscriberId": "sub_abc123",
"subscriberEmail": "customer@example.com",
"status": "active",
"enrollmentKey": "__default__"
}
],
"hasMore": true,
"message": "Moved 180 enrollments. 4140 still wait on the source step; repeat this request to release more. 320 left under the daily limit."
}
```
```json 400 theme={null}
{
"error": "fromNodeId branches into multiple next steps, so targetNodeId is required"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "Tagging moved enrollments requires the subscribers:tag scope. Omit `tags` to move them without tagging."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Pause Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/pause-enrollments
POST /api/v1/sequences/{sequenceId}/pause-enrollments
Stop new sequence enrollments while current recipients continue
Stops new subscribers from entering an active sequence. Current recipients keep
moving through the sequence until they finish.
## Request
Sequence ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/pause-enrollments" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Sequence enrollment paused",
"sequenceId": "seq_abc123",
"enrollmentPaused": true
}
```
```json 400 theme={null}
{
"error": "Sequence must be active to pause or resume new enrollments without freezing current recipients"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Preview Sequence from Example
Source: https://docs.sequenzy.com/api-reference/sequences/preview-from-example
POST /api/v1/sequences/from-example/preview
See the first emails of an email gallery sequence written for your brand
Rewrite the first 3 emails of a public sequence from the [Sequenzy email gallery](https://sequenzy.com/email-examples) and get them back without creating anything. Each email keeps the example's layout with every word written for your brand.
By default the preview is written for your company's brand profile. Pass `website` to write it for another site instead, such as a prospect or a client.
The emails are written in parallel. Any that could not be written this time are counted in `failedEmailCount`; the call fails only when none could be written. It usually takes 10 to 30 seconds. To create the sequence, call [Create Sequence from Example](/api-reference/sequences/create-from-example), which writes every email again for your own brand.
Requires the `sequences:write` scope.
## Request
Identify the example with `url`, or with `brand` and `sequence`. Sending both returns 400.
Gallery sequence page URL, such as
`https://sequenzy.com/email-examples/brands/linear/sequences/onboarding`. An
email URL returns 400; use [Preview Template from
Example](/api-reference/templates/preview-from-example) for one email.
Gallery brand slug. Use with `sequence`.
Gallery sequence slug, from `/sequences/{sequence}`. Use with `brand`.
Optional direction for every email, up to 1000 characters. It takes priority
over the example.
Optional site or domain, such as `acme.com`, to write the preview for instead
of your own brand. Its registrable domain is used. A value that is not a site
returns 400.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/from-example/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sequenzy.com/email-examples/brands/linear/sequences/onboarding"
}'
```
## Response
The unsaved rewrite: `brandName`, `website` (`null` for your own brand),
`emails`, `remainingEmailCount` (emails in the example beyond the preview) and
`failedEmailCount` (previewed emails that could not be written this time).
Each email has `stepNumber` (its position in the example, from 1), `day`,
`originalSubject`, `subject`, `previewText` and `html`.
The gallery sequence it is modeled on: `brand`, `name`, `emailCount` and
`url`.
```json 200 theme={null}
{
"success": true,
"preview": {
"brandName": "Acme",
"website": null,
"emails": [
{
"stepNumber": 1,
"day": 0,
"originalSubject": "Welcome to Linear",
"subject": "Welcome to Acme",
"previewText": "Your first project takes two minutes",
"html": "..."
}
],
"remainingEmailCount": 2,
"failedEmailCount": 0
},
"example": {
"brand": "Linear",
"name": "Linear onboarding",
"emailCount": 5,
"url": "https://sequenzy.com/email-examples/brands/linear/sequences/onboarding"
},
"message": "Preview only: the first emails, nothing saved. Call POST /sequences/from-example to create the draft sequence."
}
```
```json 404 theme={null}
{
"error": "That gallery sequence was not found. It may have been removed.",
"code": "EXAMPLE_NOT_FOUND"
}
```
```json 503 theme={null}
{
"error": "The preview could not be written. Try again in a moment.",
"code": "REMIX_FAILED"
}
```
A 400 means a missing, conflicting or invalid example reference, or a malformed `website`. A 404 with code `EMAIL_GALLERY_UNAVAILABLE` means the email gallery is not available yet. A 422 means a field failed validation.
# Realign Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/realign-enrollments
POST /api/v1/sequences/{sequenceId}/enrollments/realign-sending-window
Move waiting enrollments to the start of a changed sending window without cancelling or re-enrolling them
Changing a live sequence's sending window deliberately leaves contacts who are already waiting on an email-bound delay step alone: they keep the wake-up time their delay produced. That means a widened window never reaches them, and a narrowed window pushes them to the **next** allowed day - a full extra cycle on a weekly sequence.
This endpoint pulls those waits forward instead. It never cancels or re-enrolls anyone.
## Guarantees
* A wait only ever moves **earlier**, never later.
* A wait never moves to a different local day in the window's timezone, so nobody changes weekly cycle.
* A wait never moves earlier than now, so nothing is sent retroactively.
* Per-step "wait until weekday" gates are respected: a contact is never moved earlier than the step's own window opening.
* Sequence windows never advance SMS, webhooks, branches, or other non-email actions.
* Running it twice is a no-op. Waits already sitting at their opening report `already_at_window_start`.
## Request
Sequence ID.
Step IDs to limit realignment to. Use `nodeIds` from [Get
Sequence](/api-reference/sequences/get) or `currentNodeId` values from [List
Enrollments](/api-reference/sequences/list-enrollments). Defaults to every
step when omitted; when provided, it must contain at least one non-empty ID.
Up to 500 subscriber IDs to limit realignment to. Defaults to every waiting
contact when omitted; when provided, it must contain at least one non-empty
ID.
Opaque continuation cursor. When a response has `hasMore: true`, pass its
`nextCursor` here to continue after the enrollments already scanned.
When `true` (the default), returns the new wait times without writing them.
Set it to `false` to queue a background apply job.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/enrollments/realign-sending-window" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"dryRun": false
}'
```
## Why an enrollment did not move
`unchangedReasons` counts each reason:
| Reason | Meaning |
| - | - |
| `already_at_window_start` | The wait is already at or before that day's opening |
| `already_due` | The wait has passed and the enrollment is about to resume |
| `day_not_allowed` | The wait lands on a day the window does not cover, so moving it would change the send day |
| `no_shared_opening` | The sequence window and the step's weekday gate never overlap before the wait |
| `no_window` | Neither the sequence nor the contact's current step defines a window |
| `not_email_bound` | The delay's next action is not an email, so the sequence window does not govern it |
| `send_retry` | The email is waiting on a provider retry backoff, which realignment never shortens |
| `raced` | A worker advanced the enrollment between the scan and the write |
## Paging through a large sequence
Each job moves at most 1,000 enrollments and inspects at most 20,000. An applied request returns HTTP 202 with a `jobId`; poll `GET /api/v1/sequences/{sequenceId}/enrollments/realign-sending-window/jobs/{jobId}` until it completes. When the completed result has `hasMore: true`, pass its `nextCursor` back as `cursor` to queue the next bounded job.
```bash theme={null}
sequenzy sequences realign-enrollments seq_abc123 --apply
sequenzy sequences realign-enrollments seq_abc123 --job-id
sequenzy sequences realign-enrollments seq_abc123 --apply --cursor eyJzY2hlZHVsZWRGb3IiOiIuLi4ifQ
```
## Responses
```json 202 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"sequenceName": "52-Week Medicine Wheel",
"dryRun": false,
"status": "queued",
"jobId": "realign-sequence-enrollments-seq_abc123-...",
"message": "Enrollment realignment queued. Poll the job status endpoint for the result before continuing with nextCursor."
}
```
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"sequenceName": "52-Week Medicine Wheel",
"dryRun": true,
"sendingWindow": {
"enabled": true,
"timezone": "America/Los_Angeles",
"startTime": "08:00",
"endTime": "12:00",
"days": ["sunday"]
},
"scannedCount": 330,
"realignedCount": 330,
"unchangedCount": 0,
"unchangedReasons": {},
"requeueFailedCount": 0,
"changes": [
{
"enrollmentId": "tok_abc123",
"subscriberId": "sub_abc123",
"subscriberEmail": "customer@example.com",
"currentNodeId": "node_week_14",
"waitUntil": "2026-08-16T18:13:00.000Z",
"newWaitUntil": "2026-08-16T15:00:00.000Z",
"movedEarlierMinutes": 193
}
],
"hasMore": false,
"maxRealignmentsPerRequest": 1000,
"message": "330 waiting enrollments would move earlier. Re-run with dryRun false to apply."
}
```
```json 400 theme={null}
{
"error": "A maximum of 500 subscriberIds can be realigned per request"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "API key is missing the sequences:enroll scope"
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Render Sequence Step HTML
Source: https://docs.sequenzy.com/api-reference/sequences/render
POST /api/v1/sequences/{sequenceId}/nodes/{nodeId}/render
Render one sequence email step to email-ready HTML
Render a single sequence email step to the exact email-safe HTML that would be sent. Use this to show a visual preview of a step inside your own dashboard or email builder.
The render applies everything the send path applies: your email theme and font, company branding and footer, localization, block visibility conditions, product recommendation blocks, and merge tags.
This endpoint is read-only. It requires the `sequences:read` scope and never
sends or enrolls anyone. It uses `POST` only so personalization input can
travel in a request body.
## Request
Sequence ID.
Email step node ID. Get it from [Get Sequence](/api-reference/sequences/get).
Personalize as this stored subscriber. Use this or `subscriber`, not both.
Because the rendered HTML then carries that subscriber's details, this field
also requires the `subscribers:read` scope.
Personalize as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Tags this ad-hoc contact carries, used to evaluate `tag` block conditions.
Nothing is stored. Without it a tag condition has no tags to read, so only the
else branch of a tag split can be previewed.
Extra merge variables layered over the contact's attributes.
Force a localization locale instead of deriving it from the contact.
Render a specific A/B test variant. Required when the step is an A/B test
(`action_ab_test`) step: those steps have no email of their own, and copy
lives on the variants from [Get Sequence](/api-reference/sequences/get).
Sequence variants also need the `ab_tests:read` scope, the same scope Get
Sequence uses to return competing variant copy.
Apply your company's auto-UTM link decoration, as a real send would. Defaults
to `false` so the preview shows your own clean URLs.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/nodes/node_1/render" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "subscriberId": "sub_abc123" }'
```
## Discount codes
When the step sits downstream of a create-discount step, `{{discount.code}}` and the other `{{discount.*}}` tags resolve against that step's real configuration - percentage or amount, currency, expiry - with a placeholder `TEST-CODE` in place of the code itself. Real codes are minted per enrollment, so a preview has none to show, and this lets you check the copy before anyone is enrolled.
If the step can be reached by paths that do not all run the same discount step, the preview leaves the tags empty. A real send fills them from whatever discount that recipient's own run created, so recipients who took a path through the discount step still get a code - the preview has no way to know which path a recipient took, so it declines to guess rather than showing a code the email may not have.
## Checking merge tags
An email never shows a raw `{{TAG}}` to a recipient, so a tag that does not exist and a tag that is simply blank for this contact both render as an empty string. `unresolvedMergeTags` tells them apart:
* `"reason": "unknown"` - nothing provides that name, so it will be empty for every recipient. Usually a typo, or a tag carried over from another platform.
* `"reason": "no_value"` - the name is recognized but has no value for this contact.
A tag with a fallback is reported too when its name is unrecognized. `{{ subscriber.frstName | default: "there" }}` renders `there` for everybody, including the recipients whose first name is stored, and the HTML looks correctly personalized - so an empty `unresolvedMergeTags` array, not the rendered greeting, is what tells you personalization works. A recognized name that is merely blank for this contact is not reported when it has a fallback, since that is exactly what a fallback is for.
A name is only called `unknown` when the render had a source to check it against. Without the contact's attributes nothing is checkable at all - a bare `{{plan}}` reads the same attribute map as `{{subscriber.plan}}` - so pass a stored `subscriberId`, or an inline `subscriber` that includes `customAttributes`. Beyond that, `{{event.*}}` needs sample event properties in `variables`, since a real send fills those from the enrolling event, and `{{recommendedProducts.*}}` needs a stored `subscriberId` the catalog has something to recommend for. Without those, the tags come back as `no_value` rather than being reported as typos. An optional attribute that this contact happens not to have set is kept out of `unknown` by checking the names other contacts in the account carry, which is subscriber data: that check needs the `subscribers:read` scope, so a key without it may report such a name as `unknown`. `{{discount.*}}` is checkable only when every path into the step runs the same discount step.
An empty array means every tag in the email resolved.
## Checking conditions
Conditional blocks split on subscriber data, and the fields that read stored subscriber state - `tag`, `segment`, `list`, `status`, `event`, purchases, and engagement - are evaluated per recipient at send time. A render can evaluate them for a stored `subscriberId`, and it can evaluate a `tag` condition for an inline contact that lists its own `tags`. Anything else it cannot check.
A condition it cannot check renders as `false`, the same fail-closed rule a live send uses, so the else branch appears in the HTML exactly as it would for a contact who genuinely does not match. `unevaluatedConditions` is what tells those apart:
* `"reason": "requires_stored_subscriber"` - the field reads stored subscriber state. Pass `subscriberId`, or for a `tag` condition pass `subscriber.tags`.
* `"reason": "invalid_filter"` - the stored condition is malformed, so a real send fails it closed too. `hint` carries the validation error.
* `"reason": "evaluation_failed"` - the lookup itself failed. The render is worth retrying.
An empty array means every condition in the email was actually evaluated, so the branches in the HTML are the branches this contact would receive.
## What is not in the HTML
Per-send click-redirect wrapping, the open tracking pixel, and unsubscribe click tracking are applied at send time against a real email send record, so they are never present in the returned HTML. The `tracking` flag controls auto-UTM decoration only.
Without a subscriber the footer unsubscribe link points at a placeholder URL rather than a working token.
## Responses
```json 200 theme={null}
{
"success": true,
"html": "...",
"subject": "Day 1: getting started",
"previewText": "Your first steps",
"locale": "en",
"personalized": true,
"trackingApplied": false,
"unresolvedMergeTags": [{ "tag": "discountCode", "reason": "unknown" }],
"unevaluatedConditions": [
{
"field": "tag",
"operator": "contains",
"value": "extended",
"description": "Tag contains extended",
"reason": "requires_stored_subscriber",
"hint": "Pass subscriberId, or pass tags on the inline subscriber, to evaluate this condition."
}
],
"entity": {
"type": "sequence_step",
"id": "node_1",
"variantId": null
}
}
```
```json 400 theme={null}
{
"error": "Sequence step is not an email step"
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 404 theme={null}
{
"error": "Sequence step not found"
}
```
# Resume Sequence Enrollments
Source: https://docs.sequenzy.com/api-reference/sequences/resume-enrollments
POST /api/v1/sequences/{sequenceId}/resume-enrollments
Allow new subscribers to enter an active sequence again
Reopens new enrollment for an active sequence whose enrollment gate was paused.
Use Enable Sequence instead when the whole sequence was disabled.
## Request
Sequence ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences/seq_abc123/resume-enrollments" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Sequence enrollment resumed",
"sequenceId": "seq_abc123",
"enrollmentPaused": false
}
```
```json 400 theme={null}
{
"error": "Sequence must be active to pause or resume new enrollments without freezing current recipients"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Send Sequence Step Test
Source: https://docs.sequenzy.com/api-reference/sequences/send-test
POST /api/v1/sequences/{sequenceId}/nodes/{nodeId}/test
Send one saved action_email sequence step to internal reviewers
Queue a real test email for one saved `action_email` step without enabling the
sequence or enrolling subscribers. Use the sequence detail endpoint first to
find an email entry whose `nodeType` is `action_email`, then pass its `nodeId`.
`action_ab_test` steps are not supported by this endpoint; inspect their
variants on the sequence detail `emails[].abTest.variants` payload. You can send the same
plain email step to up to ten reviewers.
Each result includes a durable `emailSendId`; pass it to the email-send details
endpoint to inspect delivery status and provider failures.
## Request
Sequence ID containing the saved email step.
`action_email` step node ID returned by `GET /api/v1/sequences/{sequenceId}`.
Do not pass an `action_ab_test` node.
One to ten internal reviewer email addresses. Duplicate addresses are sent
only once.
```bash theme={null}
curl -X POST \
"https://api.sequenzy.com/api/v1/sequences/seq_abc123/nodes/node_email_2/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
"reviewer-one@example.com",
"reviewer-two@example.com"
]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Queued 2 sequence step test emails",
"sequenceId": "seq_abc123",
"nodeId": "node_email_2",
"results": [
{
"recipientEmail": "reviewer-one@example.com",
"emailSendId": "send_abc123",
"jobId": "123"
},
{
"recipientEmail": "reviewer-two@example.com",
"emailSendId": "send_def456",
"jobId": "124"
}
]
}
```
`jobId` is a legacy queue identifier. Use `emailSendId` for delivery status.
```json 400 theme={null}
{
"error": "A/B test steps cannot be sent with send_sequence_test_email. This endpoint supports action_email steps only; inspect variant copy on get_sequence.sequence.emails[].abTest.variants."
}
```
```json 404 theme={null}
{
"error": "Sequence step not found"
}
```
# Simulate Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/simulate
GET /api/v1/sequences/{sequenceId}/simulate
Preview who a sequence would enroll and one contact's path, without sending mail
Dry-run a sequence. Nothing is sent, no one is enrolled, and tags, lists, and discounts are not written.
Activating a sequence does **not** enroll anyone automatically. Without a subscriber this reports who currently matches and whether the sequence is ready (existing list, tag, and segment matches can still be enrolled by hand).
Pass `subscriberId` or `email` to also walk that stored contact through the graph and see which branch they take. Use that to check cases like "would a paid user get the upgrade email?"
Call this before [Enable Sequence](/api-reference/sequences/enable).
`sendsMail` is always `false`. To send one saved email step to a reviewer, use
[Send Sequence Step Test](/api-reference/sequences/send-test).
This endpoint always requires both `sequences:read` and `subscribers:read`
because aggregate results include contact samples, even when no subscriber is
selected for a path preview.
## Request
Sequence ID.
Optional stored subscriber to walk through the graph. Do not pass with
`email`.
Optional stored subscriber email to walk through the graph. Do not pass with
`subscriberId`.
How many currently matching contacts to include in the sample. Defaults to 10,
maximum 25.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/simulate" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/simulate?email=maya@example.com" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Always `false`.
Plain-language description of who enrolls after activation.
Whether an exact current-match count is available. This is `false` for
frequency and inactivity triggers, whose matches are evaluated by the hourly
worker.
Always `0`. Activating a sequence does not enroll anyone.
Contacts who match the trigger right now and are not already in the sequence.
These people are not auto-enrolled. `null` when `currentlyMatchingAvailable`
is `false`.
`false` when activation blockers are present (missing trigger, unattached
email, disconnected discount provider, and similar).
Non-blocking notes that Enable Sequence also returns, such as empty branch
paths or a `contact_added` trigger with no list, which enrolls every contact
added to your company.
Walked graph for the optional stored subscriber, or `null` when none was
passed. Each step may include an `outcome` with `verdict` (`passed`, `failed`,
or `unknown`) and `assumed` when the branch could not be known statically.
Email and A/B-test steps traversed by the selected path, whether or not the
subscriber can currently receive them.
Traversed email steps deliverable for the subscriber's current email and
subscription status. Transactional steps remain deliverable to unsubscribed
contacts; bounced and email-less contacts have no deliverable emails.
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"sequenceName": "Trial upgrade",
"status": "draft",
"sendsMail": false,
"enrollment": {
"triggerType": "tag_added",
"summary": "Anyone who gets the \"trial\" tag from now on goes through this sequence.",
"note": "The 2 people who already have the tag won't be enrolled automatically - you can enroll them manually later.",
"autoEnrollOnActivateCount": 0,
"autoEnrollTiming": "on_future_match",
"currentlyMatchingCount": 2,
"currentlyMatchingEnrollsAutomatically": false,
"currentlyMatchingSample": [
{ "id": "sub_1", "email": "maya@example.com", "name": "Maya" }
],
"error": null
},
"readiness": {
"ready": true,
"errors": [],
"warnings": []
},
"path": null
}
```
# Get Sequence Stats
Source: https://docs.sequenzy.com/api-reference/sequences/stats
GET /api/v1/sequences/{sequenceId}/stats
Get engagement metrics for a sequence
Returns aggregated engagement metrics, a live active/waiting enrollment
breakdown by current node, skipped-enrollment counts, and per-step metrics for
a sequence. You can also use
`GET /api/v1/metrics/sequences/{sequenceId}`.
For account keys used by marketers, sequences that reference transactional templates are hidden. Direct sequence or step requests return `404`; per-email lists omit these sources before totals and pagination.
## Request
Sequence ID.
Sliding time window: `1h`, `24h`, `7d`, `30d`, or `90d`.
Custom range start as an ISO 8601 date-time. Use with `end`.
Custom range end as an ISO 8601 date-time. Use with `start`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sequences/seq_abc123/stats?period=7d" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"sequenceId": "seq_abc123",
"period": "7d",
"stats": {
"sent": 120,
"delivered": 118,
"bounced": 2,
"opened": 72,
"clicked": 18,
"unsubscribed": 1,
"deliveryRate": 98.33,
"bounceRate": 1.67,
"openRate": 61.02,
"clickRate": 15.25,
"unsubscribeRate": 0.85
},
"enrollmentSkipped": {
"count": 3,
"byReason": {
"unsubscribed": 2,
"bounced": 1
}
},
"enrollmentCounts": {
"active": 4,
"waiting": 26,
"total": 30,
"byCurrentNode": [
{
"currentNodeId": "node_wait_3_days",
"currentNodeType": "logic_delay",
"currentNodeLabel": "Wait three days",
"currentNodeMissing": false,
"active": 0,
"waiting": 18,
"total": 18
}
]
},
"steps": []
}
```
`enrollmentSkipped` uses the requested `period` or `start`/`end` range. When
no explicit range is provided, it covers the last 30 days.
`enrollmentCounts` is always a live snapshot and is not constrained by those
historical date filters. It counts active and waiting enrollment runs, not
necessarily distinct subscribers, and preserves missing current-node IDs so
every live enrollment remains explainable.
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
# Start Live Sequence Test
Source: https://docs.sequenzy.com/api-reference/sequences/test-run
POST /api/v1/sequences/{sequenceId}/test-runs
Test a saved sequence with trigger event properties, including cart IDs and arrays
Run your saved sequence for one active subscriber with an email address. This performs real actions, including webhooks, subscriber updates and discounts. Emails are marked as tests. It does not enable the sequence or record a trigger event. Requires `sequences:activate` and `subscribers:read`.
Pass `subscriberId` and optionally `speedMultiplier` (integer from 60 to 10000, default 120). Existing live-test delay and wait caps apply. Supply `customVariables` as the event properties object, without an outer `event` wrapper:
```json theme={null}
{
"subscriberId": "sub_123",
"customVariables": {
"cart_id": "cart_123",
"event_ids": ["event_1", "event_2"],
"cart": { "total": 49.5 }
}
}
```
Your sequence can reference `{{event.cart_id}}` and nested values such as `{{event.cart.total}}`. Arrays remain arrays. Omit `customVariables` or pass `{}` for no event properties; `null`, scalar roots and array roots are invalid. Supplying properties does not itself update subscriber attributes. These values belong to this run and are not saved as sequence configuration. The run response has no separate event-properties field, but resolved values can appear in step logs.
The response contains `run`, with `id`, `automationId`, `companyId`, `subscriberId`, `initiatedByUserId`, `recipientEmails`, `speedMultiplier`, `status`, `steps`, `errorMessage`, `createdAt`, `updatedAt`, `startedAt`, `finishedAt` and a creation-only `jobId`. Initial status is `queued`, steps are empty and start/finish/error fields are null. Poll [Get Live Sequence Test](./test-run-get) using `run.id`.
Invalid input returns 400; missing authentication returns 401; missing scopes or restricted roles return 403. Inaccessible sequences or active subscribers return 404. An existing queued/running test for the same sequence and subscriber returns 409. Queue failure returns 500 and marks the run failed.
Ordinary failure retries are disabled. Stalled-job recovery can replay actions after worker loss. A failed run can have completed real actions, which are not rolled back. Inspect its steps before starting a new run; repeating a run can repeat emails, webhooks and other actions.
```bash theme={null}
sequenzy sequences test-run seq_123 --subscriber sub_123 --event-properties '{"cart_id":"cart_123","event_ids":["event_1","event_2"]}' --json
sequenzy sequences test-run-get seq_123 run_123 --json
```
MCP provides `start_sequence_test_run` with the same request fields and `get_sequence_test_run` with `sequenceId` and `runId`. Use [Send Sequence Step Test](./send-test) for a single email or [Simulate Sequence](./simulate) for a dry run.
# Get Live Sequence Test
Source: https://docs.sequenzy.com/api-reference/sequences/test-run-get
GET /api/v1/sequences/{sequenceId}/test-runs/{runId}
Inspect live sequence test status and step logs
Pass the sequence ID and `run.id` from [Start Live Sequence Test](./test-run). Requires `sequences:read` and `subscribers:read`.
The response contains `run` with its ID, sequence ID (`automationId`), company ID, subscriber ID (nullable on historical runs), initiating user ID (nullable), recipient emails, speed multiplier, timestamps, status, steps and nullable error message. Status progresses from `queued` to `running`, then `completed` or `failed`. `startedAt` and `finishedAt` are null until those transitions occur. Step logs include node ID, status, timing and action-specific messages or metadata when available. This endpoint does not return the creation-only queue job ID or a separate event-properties field. Resolved event values can appear in step logs.
Missing authentication returns 401, missing scopes or restricted roles return 403, and inaccessible or mismatched sequence/run IDs return 404.
Inspect failed steps before starting a new test. Completed actions are not rolled back, and running the test again can repeat them.
# Update Sequence
Source: https://docs.sequenzy.com/api-reference/sequences/update
PUT /api/v1/sequences/{sequenceId}
Update sequence identity, settings, structure, or existing nodes
If you use an account key as a marketer, choose an existing sender and
Reply-To profile. A request that needs a new profile returns `400` before
creating profiles, labels, or campaign/sequence changes. This also applies to
email steps and inserted sequence branches. An owner or admin can add the
identity first; a step-level display-name override can still use an existing
profile.
Updates sequence sending identity, metadata, auto-stop behavior, branch logic,
graph topology, linear steps, and email content. You can insert new linear steps
with `insertSteps`, restructure existing nodes with `graphEdit`, or target
any existing node with `nodeUpdates`.
## Request
Sequence ID.
Updated sequence name.
Updated dashboard description.
Replacement dashboard label names. Missing labels are created.
Whether recipients can cancel this sequence from email preferences.
Atomically replace the current trigger with `contact_added`, `tag_added`,
`segment_entered`, `segment_exited`, `event_received`, `inbound_webhook`,
`inactivity`, `frequency`, or `manual`. Include the matching trigger fields
(`listId`, `listIds`, `listScope`, `tagName`, `tagNames`, `segmentId`,
`eventName`, `propertyFilters`, `integrationSlug`, `integrationEventKey`,
`customIntegration`, `inactiveDays`, `inactivityBaseline`, `minCount`, or
`timeWindowDays`) in the same request. Active sequences require
`confirmLiveChange: true`. Trigger replacement cannot be combined with node,
content, or structural graph changes.
With a replacement `trigger: "segment_entered"`, stop the contact's enrollment
when they leave the segment. Include `trigger` and `segmentId` in the same
request; this is a trigger replacement setting, not an independent patch.
Active sequences also require `confirmLiveChange: true`.
Several list IDs for a replacement `contact_added` trigger. A contact joining
**any** of them enters the sequence. Cannot be combined with `listScope`. Up
to 25 lists.
Several tag names for a replacement `tag_added` trigger. Receiving **any** of
them enters the contact into the sequence. Up to 25 tags.
For a replacement `contact_added` trigger with no list at all. `any_contact`
enrolls every contact that is added, even one that joins no list; `any_list`
waits for a list membership. Cannot be combined with `listId` or `listIds`.
From address for every sequence email. Its domain must be configured and
verified. Mutually exclusive with `senderProfileId`.
Display name recipients see, e.g. `Brennon at TradeTally`. Selects the sender
identity of that name on `fromEmail`, creating it when the address has no
identity by that name; the mailbox's other display names, and everything
pinned to them, are untouched. Requires `fromEmail`; omit it when you send
`senderProfileId`, which already carries its own display name.
Existing sender profile ID. It already supplies both the From address and the
display name, so send it on its own and omit `fromEmail` and `fromName`. To
keep this profile under a different display name, set `fromName` on the email
steps instead, where it is a per-step override.
Reply-To address for every sequence email. Mutually exclusive with
`replyProfileId`.
Display name for the Reply-To address. Requires `replyTo`; omit it when you
send `replyProfileId`, which already carries its own display name. An address
carries one Reply-To name company-wide: if `replyTo` already has a saved
profile under a different name, that saved name is kept and the response
`warnings` array says so.
Existing reply profile ID. It already supplies both the Reply-To address and
the display name, so send it on its own and omit `replyTo` and `replyToName`.
Updated re-entry mode: `unlimited`, `one_time`, or `matching_field`.
`matching_field` is only valid for event-based sequence triggers.
Dot-path event property used by `matching_field`. Set to `null` or omit it to
use the built-in defaults.
For example, use `{"enrollmentMode":"matching_field","enrollmentFieldPath":"order.id"}` to allow one active run per order while blocking duplicate active runs for the same subscriber and order.
Set to `true` to stop new enrollments for an active sequence while current
recipients continue. Set to `false` to reopen new enrollments.
Updated local-time sending window for every email step. Set to `null` to
remove the sending restriction. When set, email steps that become due outside
the window wait until the next allowed local time.
Whether the sequence respects the company [frequency
cap](/api-reference/frequency-cap/get). When `true`, an email step is skipped
for a contact already at the cap, and the contact moves on to the next step.
Transactional steps are never skipped. Omit to leave it unchanged. Non-boolean
values return `422`.
Replace the sequence key dates (`{ timezone, dates: [{ key?, label, at }] }`,
see [Create Sequence](/api-reference/sequences/create)). Contacts already
waiting on a key-date step are re-scheduled to the new dates, in both
directions; a date moved into the past wakes them now and applies the step's
`pastAction`. Set to `null` to remove every key date. Removing a key date
(or clearing them all) is rejected with `400` while a delay step still
waits for it; change that step's wait first, or change it in the same
request with `nodeUpdates`.
Manual countdowns only. `{ audience, enabled? }` keeps enrolling people who
join the audience later (new list members, new segment matches) until the
last key date passes; they land on whatever step the countdown is at.
`audience` uses the [Enroll Audience](/api-reference/sequences/enroll-audience)
shape. Switching on enrolls the current audience right away and re-checks
every few minutes, scanning only contacts who joined since the last sync;
`startsAt` delays the first sync (pass the `scheduledFor` of a scheduled
initial enrollment); `enabled: false` keeps the audience but stops syncing.
It ends itself after the last key date (`endedReason: "countdown_ended"`).
Changing `trigger` away from `manual` switches it off in the same request
(`endedReason: "disabled"`); the response then includes `audienceAutoEnroll`.
Set to `null` to remove it.
Email addresses that receive a blind copy of every email this sequence sends,
such as a customer support inbox (max 10). BCC recipients are invisible to the
subscriber. Set to `null` to remove them.
Updated auto-stop condition, re-evaluated before every step including the
first one. Use `has_tag`, `does_not_have_tag`, `added_to_list`,
`removed_from_list`, `entered_segment`, `field_changed`, `event_received`, or
`none`. `does_not_have_tag` and `removed_from_list` stop the run whenever the
subscriber lacks that tag or list membership, so they act as a required-tag or
required-list allowlist and cancel everyone else before any step sends.
Guarded-out contacts still enroll and are then cancelled at the trigger node,
so they appear as cancellations there rather than in the active or waiting
enrollment counts. Clearing the guard does not retry them: they only receive
the sequence if the trigger fires for them again, and on the `one_time`
enrollment mode not even then. `event_received` only counts events received
after enrollment; the enrolling event and earlier history do not satisfy the
stop. Use `event_property_filter` with `propertyFilters` to require every
literal-value filter to match a later stop event, or `event_property` rules to
compare the stop event with the enrolling event. `field_changed` can use a
typed `field_value` comparison. Tag/list defaults are returned with `value:
null` and an `entry_audience` match config; that public shape can be sent back
unchanged to preserve the per-enrollment guard.
Insert an if/else branch after an existing node. Provide `afterNodeId` and a
`branches` array. Each conditional path can create `steps`, route directly to
an existing `targetNodeId`, or run steps and then reach that target. The else
path uses `elseSteps` and/or `elseTargetNodeId`. Use the completion node ID
returned by the sequence GET endpoint to end one path immediately. Empty
branch paths are rejected unless you set `allowEmptyPaths: true`. Branch
`conditionType` values include `has_tag`, `does_not_have_tag`, `in_list`,
`in_segment`, `event_received`, `link_clicked`, and field comparisons. For
`has_tag` and `does_not_have_tag`, provide `tagId` or `tagName`. For
`event_received` and `link_clicked`, set `activityScope` to `this_sequence`,
`previous_email`, or `ever`; omitting it checks `ever`. Path `steps` use the
same shape as `insertSteps`, so an action inside a path carries its fields in
`config` - for example `{ "nodeType": "action_add_tag", "config": { "tagName":
"newsletter-confirmed" } }`.
Set `splitMode` to `random` to make the branch a weighted A/B split instead of
an if/else. Each subscriber is then assigned a path by percentage when they
reach the node, so no subscriber attribute is evaluated. A random split
requires `randomPercentages` with one non-negative value per entry in
`branches`, in the same order, summing to 100. Its paths must omit
`conditionType` and condition-specific fields, and it has no else path, so `elseSteps` and
`elseTargetNodeId` are rejected. To test two versions of a single existing
email step and have a winner picked automatically, create an A/B test on that
step instead.
Insert one or more new linear steps into the sequence. Provide `afterNodeId`
to insert after a specific node from `GET /api/v1/sequences/{sequenceId}`. If
`afterNodeId` is omitted, the steps are appended only when the sequence has
exactly one linear tail. Linear insertion supports every non-trigger addable
step type that does not require companion records: email, SMS, delay, tag/list
actions, attribute updates, discounts, conditions, wait-for-event steps, and
webhooks. SMS steps use `type: "sms"` with a plain-text `text` field (optional
`label`, `ineligibleAction`, `imageUrls`); generate copy with the [Generate
SMS endpoint](/api-reference/generate/sms). Email and delay steps can use
fixed `delay`/`delayMs` waits, dynamic `waitUntil` date-field waits (from
the trigger event or, with `source: "attribute"`, a contact attribute, plus
an optional `pastAction` of `continue`, `skip`, or `exit`), or
`waitUntilWeekday` waits that hold the step until the next occurrence of a
weekday inside a local-time window (for example `{ "day": "sunday",
"startTime": "09:00", "endTime": "12:00", "timezone":
"America/Los_Angeles" }` - contacts already inside the window continue
immediately). Use
`nodeType: "logic_wait_for_event"` with a config containing `eventName`,
`timeoutDays` (1-365), `timeoutAction` (`continue` or `exit`), and an optional
`label` to insert an event gate. Event receipt and a continuing timeout both
use the gate's single next path; `timeoutAction: "exit"` completes the
enrollment on timeout. Tag, list, subscriber-update, condition, and webhook
node types carry their fields in `config`: `action_add_tag` and
`action_remove_tag` take `tagName`, or `tagId` when you have the real tag ID
(a missing tag definition is created for you), `action_add_to_list` and
`action_remove_from_list` take `listId`, `action_update_attributes` takes the
Update Subscriber fields, `logic_condition` takes `conditionType` plus that
condition's resource field, and `action_webhook` takes an HTTPS `url` with
optional `method` (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`), `headers`, a
JSON `body` template (valid JSON as written, with merge tags inside quoted
values), a `resultKey` that saves the response so later email
steps can use `{{webhooks.KEY.data.field}}` merge tags, and `onError`
(`continue`, `exit`, or `fail`). Webhook `url`, `headers`, and `body` support
merge tags resolved when the step runs. `action_ai` takes a merge-tag
`prompt` (max 8000 chars), a required `resultKey`, `outputFields` (1-10
entries of `{ key, description, maxLength, fallback }` - `fallback` is used
when generation fails so emails still send; combined field limits must fit
the step's 2000-token response budget), optional `includeTags`,
`includeEventProperties`, `includeRecentEvents` (with `recentEventLimit`,
1-50, default 10), and `includeAttributes` context selectors, and
`onError` (default `continue`); later steps read the output with
`{{ai.KEY.field}}` merge tags. SMS, discount, and delay steps keep using their
step-level fields described above. Use `branch` for if/else paths. Provide
only one of `branch`, `insertSteps`, or `graphEdit` in a request. Inserted
email steps inherit the effective sender identity (sender profile, display
name, and Reply-To) of the nearest sequence email. Across a branch merge, only
identity fields shared by every incoming path are inherited; conflicting
fields use the sequence or company defaults. Set `senderProfileId`/`fromEmail`
or `replyProfileId`/`replyTo` on a step to override the inherited identity.
Restructure existing sequence nodes. Set `action` to `move_node`,
`duplicate_node`, `delete_node`, or `replace_edges`, and include the
`expectedRevision` returned as `graphRevision` by the latest
`GET /api/v1/sequences/{sequenceId}` response. A stale revision is rejected.
`move_node` and `duplicate_node` require `nodeId` plus exactly one of
`afterNodeId` or `beforeNodeId`. `beforeNodeId` funnels every incoming path
through the moved or copied node, so it can relocate an A/B test below paths
that converge at a shared continuation. Duplicated email and A/B test steps
receive independent content records; A/B metrics and winner state reset.
`delete_node` automatically reconnects a linear step. To delete a split node,
also provide the complete replacement `edges`. `replace_edges` always requires
the complete normalized topology returned by the sequence GET endpoint and
supports atomic path reconnects or multi-node reorders. Deleting an A/B test
step also removes its test, variants, and variant localizations unless the test
is currently in progress or replacement variants are still being generated.
Preserve `condition.branchId` on every edge leaving a branch node. Graph edits
reject cycles, disconnected nodes, invalid branch lanes, and changes that
remove the trigger/end path.
Deleting an otherwise-deletable step that currently holds active or waiting
recipients moves those recipients to the step's next step in the committed
topology - including a rerouted path supplied via replacement `edges` - and
processes them immediately; the response reports the count as
`migratedRecipientCount` (or `completedRecipientCount` when the deleted step has
no continuation). Trigger and end steps remain protected, as do A/B test steps
while variants are generating or a test is active. Recipient migration is also
refused when recipients could continue to more than one surviving step (for
example a merge step whose incoming paths are rerouted to different steps),
because the platform will not pick a path for them.
Required when inserting linear steps, branches, or applying `graphEdit` to an
active sequence. Set it to `true` only after confirming the live-flow impact
for current and future recipients. Content-only email edits do not require
this flag.
Atomic, type-aware patches for existing nodes. Each item requires `nodeId`
and a non-empty `changes` object. Pass the `updatedAt` returned for that node
by `GET /api/v1/sequences/{sequenceId}` as `expectedUpdatedAt` to prevent a
stale write. A node may appear only once, and `nodeUpdates` cannot be combined
with legacy email/SMS/subscriber step arrays or structural edits.
Each node read also includes `updateHints`, which lists that node type's
editable fields, managed fields, special restrictions, and the exact
`expectedUpdatedAt` token to return.
`logic_delay` accepts exactly one of `delay` (`days`, `hours`, `minutes`),
`delayMs`, `waitUntil` (`field`, optional `source`, offset, `direction`,
`missingAction`, and `pastAction`), `waitUntilWeekday` (`day`/`days`,
`startTime`, optional `endTime`, `timezone`), or `waitUntilKeyDate`. Each
replaces the whole wait, so omitted fields return to their defaults: resend
`source: "attribute"` and `pastAction` when changing an attribute wait. `action_email` accepts name/label, subject, preview
text, HTML or blocks, `emailPreset` (`branded` or `minimal`), `emailTheme`,
transactional mode, and per-step sender/reply identity. Setting `emailPreset` transforms that linked
email's native blocks using the same **Style > Format** behavior as the
dashboard without changing the company default. Native block emails may include
supported custom HTML blocks. An email stored entirely as one standalone raw
HTML block does not support `emailPreset`, and `emailPreset` cannot be combined
with `html` or `htmlContent`. A `blocks` patch that omits `emailPreset` never
changes the step's format - see the `emails` parameter below. A format switch is
not a lossless toggle: `minimal` deletes standalone logo blocks, so switching
back to `branded` generates a new logo block with a new `id` and the company
name as its `alt` text. Send the authored logo block in `blocks` alongside the
`branded` update to keep it. Both directions are reported in the response
`warnings`.
`emailTheme` restyles that one linked email and leaves the company default and
every other email untouched. It is a partial patch merged into the email's
current theme, so `{"colors": {"background": "#ffffff"}}` repaints the
outer canvas and keeps the email's fonts, spacing, and other colors. Use
`colors.content` for the inner content card and `colors.surface` for nested
cards and tinted tiles. A step with
no override merges into the company theme rather than the platform preset.
Send `null` to drop the override so the step follows the company theme again.
Supported sections are `presetId`, `buttonStyle`, `colors`, `typography`, and
`layout`; colors must be 6-digit hex, and numeric values are clamped to their
supported ranges. Use `PATCH /api/v1/companies/{companyId}` to move the
account-wide default instead.
SMS and every other stored node type accept their type-specific editable
config fields. Omitted fields are preserved. Managed linked-resource IDs,
node-type conversion, and branch path IDs/count are rejected; use `graphEdit`
for topology changes. Webhook header patches merge with existing headers, and
redacted values from the read response cannot be written back.
Required with `nodeUpdates` when the sequence is active. Set it to `true` only
after confirming the behavior change. Existing waiting recipients retain their
already-calculated `scheduledFor`; a new delay applies when recipients reach
that node after the update.
Existing email step updates. Provide `emails` or `steps`, not both. Items
without `nodeId` or `emailId` are matched by existing step order and do not
create new steps. `html` and `htmlContent` are preserved as one raw HTML
block; provide `blocks` for native Sequenzy block content. Each item can also
set `emailPreset` to `minimal` or `branded` when working with native blocks,
including emails that contain supported custom HTML blocks. Emails stored
entirely as one standalone raw HTML block do not support format changes. Each
item can also set `emailTheme` to override that one email's theme, with the
same partial-patch and `null`-clears rules described under `nodeUpdates`. Each
item can also set per-step sender identity: `senderProfileId` or `fromEmail`
(+ optional `fromName`) for the From identity, and `replyProfileId` or
`replyTo` (+ optional `replyToName`) for the Reply-To. A `fromName` on its own
only changes the visible display name. Per-step identity overrides the
sequence-level sender for that step. Email items can also set
`isTransactional`, `ccEmails`, and `bccEmails` to match the dashboard
delivery settings, and `attachments` (`[{ filename, path }]`) to configure
URL-backed file attachments fetched at send time. For event-triggered
sequences, `path` may use `{{event.file_url}}` and `filename` may also use
event merge tags; values come from the event that enrolled each subscriber
(max 10, 15MB total per email; an empty array removes them).
`blocks` are validated against the block schema, exactly as on create. A block
that is structurally invalid is rejected with `400` naming the step, the block
index, and the field, rather than stored - a stored block the renderer cannot
render fails every future send of that step. Fields that parse but will not
render as their name suggests come back in an advisory `warnings` array on the
`200`. Email steps created through `branch` or `insertSteps` are checked the
same way; their warning paths are rooted at the path label, for example
`If.steps[0].blocks[0]` and `Inserted.steps[0].blocks[0]`.
Replacing `blocks` keeps that step's existing **Style > Format** rather than
the company default. A step that already had a logo or a footer gets them back
even when you omit them, and every block added that way is named in the
`warnings` array, so you can see what landed without re-reading the sequence.
Send `emailPreset` to change the format instead. A step stored as one
standalone raw HTML block has no format, so replacing it with another
standalone raw HTML block stores it exactly as sent and nothing is added.
Replacing that markup with native blocks is a conversion rather than a markup
edit, so the new blocks do get a footer.
Alias for `emails`. Use `insertSteps` to create new steps.
Content updates for existing SMS steps, targeted by `nodeId` (an `action_sms`
node from `GET /api/v1/sequences/{sequenceId}`). Each entry can set `text` (or
`blocks`), `imageUrls` (with `text`), `label`, and `ineligibleAction` (`skip`
or `exit`). SMS content edits are content-only and do not require
`confirmStructuralChange`. Use `insertSteps` to create new SMS steps.
Full config replacements for existing Update Subscriber steps, targeted by
an `action_update_attributes` node ID from
`GET /api/v1/sequences/{sequenceId}`. Values can use standalone event merge
tags such as `{{event.plan}}`; number and boolean tags are coerced after
resolution.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Updated Welcome Sequence",
"stopCondition": {
"type": "has_tag",
"value": "customer"
},
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"insertSteps": {
"afterNodeId": "node_migration_email",
"steps": [
{
"delay": { "days": 1 },
"name": "Migration check-in",
"subject": "Need help migrating?",
"html": "
"
}
]
}'
```
Suppress a follow-up after a reply by routing the matching path to completion
and the else path to the existing second email:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"branch": {
"afterNodeId": "node_email_1",
"label": "Did they reply?",
"branches": [
{
"id": "replied",
"conditionType": "event_received",
"eventName": "email.replied",
"activityScope": "this_sequence",
"targetNodeId": "node_sequence_complete"
}
],
"elseTargetNodeId": "node_email_2"
}
}'
```
Move an existing A/B test below paths that converge at `node_end`:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"graphEdit": {
"action": "move_node",
"expectedRevision": "48ab6d78a2d4f2b7d2d36d8e1dba9ea3f411bb65059b627a2b8268b4e81d8f7a",
"nodeId": "node_ab_test",
"beforeNodeId": "node_end"
}
}'
```
Atomically change two existing delays from five minutes to seven days:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sequences/seq_abc123" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"nodeUpdates": [
{
"nodeId": "node_delay_1",
"expectedUpdatedAt": "2026-07-14T10:00:00.000Z",
"changes": { "delay": { "days": 7 } }
},
{
"nodeId": "node_delay_2",
"expectedUpdatedAt": "2026-07-14T10:01:00.000Z",
"changes": { "delay": { "days": 7 } }
}
]
}'
```
## Warnings
Like [Create Sequence](/api-reference/sequences/create#request-shape-and-warnings),
a successful update that included fields the API ignores names each one in
`warnings`: an unknown field such as `triggerConfig`, or a step `delay`
that is not an object. A `contact_added` `listId` or `listIds` value not found
in your company is also reported, because enabling the sequence is rejected
until the trigger uses an existing list. `companyId` and `sequenceId` in the
body are only reported when they differ from the selected company and the path.
## Responses
```json 200 theme={null}
{
"success": true,
"sequence": {
"id": "seq_abc123",
"name": "Updated Welcome Sequence",
"status": "draft",
"enrollmentPaused": false,
"updatedEmailCount": 1,
"updatedSubscriberStepCount": 1,
"senderProfileId": "sender_abc123",
"fromName": "Acme",
"fromEmail": "hello@example.com",
"replyProfileId": "reply_abc123",
"replyToName": "Support",
"replyToEmail": "support@example.com",
"stopCondition": {
"type": "has_tag",
"value": "customer"
},
"sendingWindow": {
"enabled": true,
"timezone": "Europe/Kiev",
"startTime": "08:00",
"endTime": "20:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
},
"keyDates": null,
"audienceAutoEnroll": null,
"insertedNodeIds": ["node_inserted_email"],
"insertedEmailIds": ["email_inserted"],
"insertedEmailCount": 1
}
}
```
```json 400 theme={null}
{
"error": "Active sequence structural changes require explicit confirmation. Re-run with confirmStructuralChange: true after confirming this can affect current and future sequence recipients."
}
```
```json 400 theme={null}
{
"error": "insertSteps.afterNodeId is required when the sequence does not have exactly one linear tail"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "No companies found. Create a company first using the create_company tool."
}
```
```json 404 theme={null}
{
"error": "Sequence not found"
}
```
## Retiming retries
If countdown retiming cannot be queued, this endpoint returns `503` and rolls
back the sequence edit, including other sequence fields in that request. Retry
the same update after the service recovers. Existing waiting contacts keep their
previous schedule until a date edit succeeds.
# Get Shopify Automation Settings
Source: https://docs.sequenzy.com/api-reference/shopify/get-automation-settings
GET /api/v1/shopify/automation-settings
Get the connected Shopify store's browse-abandonment, cart-abandonment, and price-drop settings
Returns the effective automation settings for the company's connected Shopify
store, with platform defaults applied where the store hasn't overridden them.
## Request
No parameters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/shopify/automation-settings" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"integrationId": "int_123",
"shopDomain": "my-store.myshopify.com",
"browseAbandonment": {
"enabled": true,
"delayHours": 2,
"cooldownHours": 24
},
"cartAbandonment": {
"enabled": true,
"delayHours": 1,
"cooldownHours": 24,
"expireAfterHours": 72
},
"priceDrop": {
"enabled": true,
"minPercent": 5,
"lookbackDays": 30,
"cooldownDays": 7
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Shopify integration not found"
}
```
# Update Shopify Automation Settings
Source: https://docs.sequenzy.com/api-reference/shopify/update-automation-settings
PUT /api/v1/shopify/automation-settings
Update the connected Shopify store's browse-abandonment, cart-abandonment, and price-drop settings
Partial update: omitted sections are left untouched, omitted fields within a
section keep their current value, and passing `null` for a section resets it
to the platform defaults.
## Request
Browse-abandonment settings: `enabled` (boolean), `delayHours` (hours to wait
after a product view, default 2, max 168), `cooldownHours` (minimum hours
between events per subscriber, default 24, max 720). Pass `null` to reset to
the defaults.
Cart-abandonment settings: `enabled` (boolean), `delayHours` (hours of cart
inactivity before the cart counts as abandoned, default 1, max 168),
`cooldownHours` (minimum hours between events per subscriber, default 24, max
720\), `expireAfterHours` (hours after which an untouched tracked cart is
dropped rather than emailed about, default 72, max 720). Pass `null` to reset
to the defaults.
Price-drop settings: `enabled` (boolean), `minPercent` (minimum price decrease
percent, default 5, max 95), `lookbackDays` (viewer audience window, default
30, max 90), `cooldownDays` (minimum days between events per subscriber and
product, default 7, max 90). Pass `null` to reset to the defaults.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/shopify/automation-settings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"priceDrop": {"minPercent": 10}, "browseAbandonment": {"enabled": false}}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Shopify automation settings updated.",
"integrationId": "int_123",
"shopDomain": "my-store.myshopify.com",
"browseAbandonment": {
"enabled": false,
"delayHours": 2,
"cooldownHours": 24
},
"cartAbandonment": {
"enabled": true,
"delayHours": 1,
"cooldownHours": 24,
"expireAfterHours": 72
},
"priceDrop": {
"enabled": true,
"minPercent": 10,
"lookbackDays": 30,
"cooldownDays": 7
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide browseAbandonment, cartAbandonment, and/or priceDrop settings to update."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/cartAbandonment/delayHours",
"message": "Expected number to be less or equal to 168"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Shopify integration not found"
}
```
# Remove SMS Number
Source: https://docs.sequenzy.com/api-reference/sms/delete-number
DELETE /api/v1/sms/numbers/{numberId}
Release a toll-free number back to the carrier
Releases a toll-free number back to the carrier via Telnyx and frees its slot
under the workspace's 100-number cap. **This is irreversible** - once released,
the number cannot be reclaimed. Campaigns, sequence steps, and automations
still pinned to the released number will **skip their SMS sends** until you
point them at another verified number - an explicit "send from" choice is
never silently swapped for a different number. Sends that never picked a
specific number keep using the workspace default (oldest verified) number.
Use this when a number's verification was rejected and you want to purchase a
different one, or when a brand's number is no longer needed. This mutation
requires the `companies:manage` scope.
## Request
The SMS number ID, as returned by [Get SMS
Settings](/api-reference/sms/settings).
No request body. Pass `X-Company-Id` if your API key has access to multiple
companies.
```bash theme={null}
curl --request DELETE "https://api.sequenzy.com/api/v1/sms/numbers/num_abc123" \
--header "Authorization: Bearer $SEQUENZY_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"number": {
"id": "num_abc123",
"status": "released"
},
"message": "SMS number released."
}
```
```json 400 theme={null}
{
"error": "This number has already been released."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
```json 404 theme={null}
{
"error": "SMS number not found"
}
```
```json 409 theme={null}
{
"error": "A number purchase is still in progress. Wait for it to finish before releasing."
}
```
```json 500 theme={null}
{
"error": "Failed to release SMS number"
}
```
```json 503 theme={null}
{
"error": "Failed to release the number with the carrier."
}
```
# Get SMS Settings
Source: https://docs.sequenzy.com/api-reference/sms/settings
GET /api/v1/sms/settings
Get the SMS add-on status for a company
Returns the company's SMS add-on status: whether SMS is enabled, plan eligibility, credit balance, brand prefix, phone numbers, and whether SMS sequence steps will actually send.
Check this before adding SMS steps to sequences. Steps added while SMS is not ready are stored normally but skip at runtime until the add-on is enabled and a number is active.
## Request
No request body. Pass `X-Company-Id` if your API key has access to multiple companies.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sms/settings" \
-H "Authorization: Bearer seq_live_xxx"
```
## Update a number
Use the number `id` returned above to set a label that appears in SMS campaign
pickers, and/or a per-number `brandPrefix` override (messages send as
`{prefix}: your message`; `null` clears back to the account-wide prefix).
Omitted fields keep their value. This mutation requires the
`companies:manage` scope.
```bash theme={null}
curl --request PATCH "https://api.sequenzy.com/api/v1/sms/numbers/num_abc123" \
--header "Authorization: Bearer $SEQUENZY_API_KEY" \
--header "Content-Type: application/json" \
--data '{"label":"Marketing","brandPrefix":"Acme Support"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"sms": {
"enabled": true,
"planEligible": true,
"creditsBalance": 4200,
"brandPrefix": "Acme",
"numbers": [
{
"id": "num_abc123",
"e164": "+18885550123",
"label": "Marketing",
"brandPrefix": null,
"status": "active"
}
],
"readyToSend": true
},
"message": "SMS is enabled and ready to send."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Company not found"
}
```
```json 500 theme={null}
{
"error": "Failed to load SMS settings"
}
```
# Send Test SMS
Source: https://docs.sequenzy.com/api-reference/sms/test
POST /api/v1/sms/test
Send a test SMS to a phone number
Sends a real test text message to any phone number. Test sends charge credits, bypass quiet hours, are excluded from step stats, and are limited to 100 per company in a rolling 24-hour window.
Requires the SMS add-on with a verified number - check [Get SMS Settings](/api-reference/sms/settings) first.
## Request
Destination phone number in international E.164 format, e.g. `+15550100123`.
Plain-text message body. Provide `text` or `blocks`, not both.
Up to 2 publicly reachable image URLs sent as MMS media (US/CA destinations
only). Only valid together with `text`.
SMS content blocks (text + image subset). Provide `text` or `blocks`, not
both.
Active SMS number ID to send from (see [Get SMS
Settings](/api-reference/sms/settings)). Defaults to your workspace's oldest
verified number - the same default real sends use. Returns a `400` if the
number is not found or not verified.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sms/test" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+15550100123",
"text": "Test from Sequenzy"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"smsSendId": "sms_abc123",
"toPhone": "+15550100123",
"message": "Test SMS queued to +15550100123. Test sends bypass quiet hours, charge credits, and are excluded from step stats."
}
```
```json 400 theme={null}
{
"error": "Enter a valid phone number in international format, e.g. +15550100123"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "The SMS add-on is not enabled for this company. Enable it in Settings > SMS."
}
```
```json 429 theme={null}
{
"error": "Test send limit reached (100 per 24 hours). Try again later."
}
```
```json 500 theme={null}
{
"error": "Failed to send test SMS"
}
```
# Get SMS Usage
Source: https://docs.sequenzy.com/api-reference/sms/usage
GET /api/v1/sms/usage
Per-number SMS send volume, delivery outcomes, and credits spent
Returns send volume and outcomes broken down **per phone number**, the same
breakdown shown on each number's detail page in **Settings → SMS**. Use it to
see what a specific number actually costs and how healthy its delivery is on
its own, without pulling raw send records.
Test sends are counted separately in `testSends` and excluded from
`totalSends`, `delivered`, `failed`, and `creditsCharged`.
## Request
No request body. Pass `X-Company-Id` if your API key has access to multiple
companies.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sms/usage" \
-H "Authorization: Bearer seq_live_xxx"
```
## Responses
```json 200 theme={null}
{
"success": true,
"usage": [
{
"fromNumber": "+18885550123",
"totalSends": 1204,
"delivered": 1180,
"failed": 8,
"creditsCharged": 1204,
"lastSentAt": "2026-08-28T14:32:00.000Z",
"testSends": 6
}
]
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 500 theme={null}
{
"error": "Failed to load SMS usage"
}
```
# List Subscriber Attributes
Source: https://docs.sequenzy.com/api-reference/subscribers/attributes
GET /api/v1/subscribers/attributes
List the custom attribute names your contacts use, with value types and examples
Lists the custom attributes in use across your account. Use it before you [create](./create) or [update](./update) a subscriber so you reuse existing names and send values of the same type. For example, if `zip` is a `string`, send `"02134"` rather than the number `2134`, which would drop the leading zero.
Value types and examples come from up to 100 of your most recent contacts that have custom attributes. Names that only older contacts carry are listed too, after the sampled ones, with `sampledContacts` set to `0`. Those come from an account-wide attribute index that returns up to the 500 most widely used names. Reserved profile fields such as email, first name and last name, and internal attributes, are not listed.
Requires the `subscribers:read` scope.
## Request
Set to `true` to also list nested paths such as `profile.tier`. Must be `true`
or `false`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/attributes" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response
Attributes seen on the sampled contacts, most common first, followed by names
that only older contacts carry.
Attribute name, or a dot path such as `profile.tier` when
`includeNested=true`.
JSON type of the example value: `string`, `number` or `boolean`. For a list
attribute, the type of its items. Always `string` when `mixedTypes` is `true`.
For names that only older contacts carry, an example of `true` or `false` is
reported as `boolean`.
Whether the attribute holds a list of values on the sampled contacts. Always
`false` for names that only older contacts carry, because the attribute index
does not record list shape.
`true` when the sampled contacts hold different types for this attribute, for
example zip codes stored as numbers on some contacts and strings on others.
`valueType` is then `string`, the only type that keeps every value intact.
Send values with leading zeros as strings either way.
An example value as text, truncated to 50 characters. List examples from
sampled contacts show up to three items. `null` when no example is available.
How many of the sampled contacts carry the attribute. `0` when only older
contacts carry it.
How many recent contacts with custom attributes were sampled.
## Errors
If the account-wide attribute index is briefly unavailable, the response still succeeds with the attributes found on the sampled contacts. A `500` means the attributes could not be read at all. The request only reads data, so you can safely retry it.
```json 200 Success theme={null}
{
"success": true,
"attributes": [
{
"key": "zip",
"valueType": "string",
"isArray": false,
"mixedTypes": false,
"sampleValue": "02134",
"sampledContacts": 42
},
{
"key": "seats",
"valueType": "number",
"isArray": false,
"mixedTypes": false,
"sampleValue": "5",
"sampledContacts": 18
},
{
"key": "interests",
"valueType": "string",
"isArray": true,
"mixedTypes": false,
"sampleValue": "running, cycling",
"sampledContacts": 7
},
{
"key": "legacy_score",
"valueType": "number",
"isArray": false,
"mixedTypes": false,
"sampleValue": "42",
"sampledContacts": 0
}
],
"sampledContacts": 100
}
```
```json 400 Bad Request theme={null}
{
"success": false,
"error": "includeNested must be true or false"
}
```
```json 401 Unauthorized theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 Forbidden theme={null}
{
"success": false,
"error": "API key is missing required scope: subscribers:read"
}
```
```json 500 Server Error theme={null}
{
"success": false,
"error": "Failed to list attributes"
}
```
# Create Subscriber
Source: https://docs.sequenzy.com/api-reference/subscribers/create
POST /api/v1/subscribers
Create a new subscriber or update an existing one based on duplicate strategy
Create a new subscriber or handle existing ones based on the `duplicateStrategy` parameter.
## Permissions
The API key needs `subscribers:write`, which also covers the `lists` array
in this request. Explicit sequence enrollment and writes that can send a
double opt-in confirmation require `automations:trigger`. Imports into explicit lists and dedicated
list membership endpoints still require `lists:write`.
## Request Body
Subscriber delivery email address. Required when creating a new subscriber
unless `phone` is provided - a contact created with only a phone becomes a
phone-only (SMS) contact that email sends automatically skip.
Your app/customer/user ID for this subscriber. It is unique within your
workspace and is trimmed without lowercasing.
First name
Last name
IANA timezone identifier such as `America/New_York`, stored as a native
profile field. Enables recipient-local campaign delivery ("send when it's 8pm
for the customer"). An invalid identifier returns `400 VALIDATION_ERROR`;
`null` clears the stored value.
Phone number in E.164 format (`+15551234567`) or national format. Stored
normalized to E.164 on the native phone field, not as a custom attribute. An
invalid phone returns `400 VALIDATION_ERROR`. Setting a phone does NOT
subscribe the contact to SMS - use `smsConsent` for that. With no `email` or
`externalId`, the phone creates or matches a phone-only (SMS) contact.
ISO 3166-1 alpha-2 country used to read a national-format `phone`, such as
`IT` or `US`. Defaults to `US`. This is a parsing hint only: the stored
`phoneCountry` always comes from the parsed number. Sending it without `phone`
returns `400 VALIDATION_ERROR`.
SMS marketing consent. `true` sets the subscriber's SMS status to `subscribed`
with consent source `api` - only send `true` when you have express written
consent. `false` sets it to `unsubscribed`. Omitted leaves SMS status
unchanged. Consent is never inferred from phone presence. See [SMS &
MMS](/concepts/sms) for the full consent model.
Status: `active`, `unsubscribed`, or `bounced`.
Consent mode: `default` obeys your workspace double opt-in setting for new
active subscribers, `confirmed` creates an active subscriber immediately when
you have verified consent, and `double_opt_in` sends a confirmation email and
keeps the contact unsubscribed until they confirm.
Omitting `optInMode` follows the workspace double opt-in setting. Use
`confirmed` only when your app already verified consent. When confirmation is
required, Sequenzy applies requested lists and tags only after the subscriber
clicks the confirmation link. `default` does not send confirmation email to an
existing unsubscribed contact.
Array of tag names to assign
Array of list IDs to add the subscriber to. If omitted, a subscriber this call
creates follows your workspace default lists setting, and a subscriber that
already exists keeps the list memberships they have - an attribute-only upsert
never changes memberships. If set to an empty array `[]`, the subscriber will
not be added to any lists.
Custom key-value attributes
Whether to enroll the subscriber in matching sequences. Defaults to `true`, or
to `false` when you supply `createdAt`. Explicitly passing `true` requires the
`automations:trigger` scope; the request returns `403` when that scope is
missing.
ISO 8601 date the contact originally signed up, for importing history from
another platform. New-subscriber account notifications remain eligible when
the signup date is at most one hour old; older dates do not notify on
creation. Double opt-in confirmation can notify even for imported contacts.
Your notification preferences, double opt-in and the daily cap still apply.
See [Preserving the original signup
date](#preserving-the-original-signup-date).
How to handle existing subscribers: - `skip` - Don't update existing
subscribers (default) - `merge` - Only fill in missing fields, never overwrite
existing values - `overwrite` - Replace all fields (but never reactivate
unsubscribed users)
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"firstName": "John",
"lastName": "Doe",
"phone": "+15551234567",
"smsConsent": true,
"tags": ["lead", "saas.monthly"],
"lists": ["list_abc123"],
"enrollInSequences": true,
"duplicateStrategy": "skip"
}'
```
## Responses
```json 200 New subscriber theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"firstName": "John",
"lastName": "Doe",
"phone": "+15551234567",
"phoneCountry": "US",
"smsStatus": "subscribed",
"status": "active",
"tags": ["lead", "saas.monthly"],
"createdAt": "2024-01-15T10:30:00Z",
"created": true,
"updated": false,
"skipped": false
}
}
```
```json 200 Double opt-in queued theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"firstName": "John",
"lastName": "Doe",
"status": "unsubscribed",
"tags": [],
"createdAt": "2024-01-15T10:30:00Z",
"created": true,
"updated": false,
"skipped": false
},
"optIn": {
"required": true,
"emailQueued": true
}
}
```
```json 200 Existing subscriber (skipped) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"tags": ["existing-tag"],
"createdAt": "2024-01-10T08:00:00Z",
"created": false,
"updated": false,
"skipped": true
}
}
```
```json 200 Existing subscriber (merged) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"tags": ["existing-tag", "lead", "saas.monthly"],
"createdAt": "2024-01-10T08:00:00Z",
"created": false,
"updated": true,
"skipped": false
}
}
```
```json 400 theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid phone number. Provide a valid phone in E.164 format, e.g. +15551234567, or US national format."
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: subscribers:write"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Subscriber identity conflict: email and externalId point to different subscribers."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Preserving the Original Signup Date
By default a new contact's signup date is the moment you created them. That is
right for a real signup and wrong for a migration: if you import 50,000
contacts today, all 50,000 look like they joined today, and an audience like
"signed up in the last 30 days" matches your entire list on day one and nobody
on day 31.
Pass `createdAt` to keep their real date:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"createdAt": "2019-03-14T09:30:00Z"
}'
```
Three things to know:
* **It only ever moves earlier.** If the contact already exists with an earlier
date, yours is ignored - an earlier date is more history than we had, a later
one would erase some. This holds for every `duplicateStrategy`.
* **Sequences do not run.** Supplying `createdAt` defaults `enrollInSequences`
to `false`, because you are describing the past, not a signup happening now.
Pass `enrollInSequences: true` with the `automations:trigger` scope to
override; the request returns `403` when that scope is missing.
* **Only the signup date moves.** The contact's "last updated" time stays the
real one, so recently-changed views still tell the truth.
Any list you add them to is joined at the same date. To import their event
history too, see [Trigger Event](/api-reference/subscribers/events/trigger).
For a whole file at once, put `createdAt` on each record in
[Create Subscriber Import](/api-reference/subscribers/import-create).
Learn how confirmation emails, pending subscribers, and signup tags work.
Learn how SMS consent, quiet hours, and message credits work.
# Delete Subscriber
Source: https://docs.sequenzy.com/api-reference/subscribers/delete
DELETE /api/v1/subscribers/{email}
Delete a subscriber
Permanently delete a subscriber by email address. You can also delete by external ID with `DELETE /api/v1/subscribers/external?externalId={externalId}`.
## Path Parameters
Subscriber email (URL encoded)
## Query Parameters
External ID to use with `/subscribers/external`. Use this route for IDs that
contain `/`.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/subscribers/user%40sequenzy.com" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"deleted": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "externalId is required"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Import Events (Many Subscribers)
Source: https://docs.sequenzy.com/api-reference/subscribers/events/import
POST /api/v1/subscribers/events/imports
Record events for many subscribers in one request
Record a bounded batch of up to 25 events for many subscribers in one request - the
pipeline companion to [Trigger Event](/api-reference/subscribers/events/trigger)
and [Trigger Events (Bulk)](/api-reference/subscribers/events/trigger-bulk),
built for data pipelines and warehouse syncs. Each event carries its own
subscriber identity.
Events are grouped per contact:
* A contact whose events are all more than an hour old is imported as
**history** in one idempotent write: stored with real timestamps, visible to
segments and the timeline, but no sequences, sync rules, waiting steps,
goals, webhooks, or double-opt-in confirmation emails run.
* A contact with any recent event takes the **live** path with normal side
effects, processed in order.
Contacts process independently, so an error response may still include
recorded events. Treat errors as partial success and retry the same request -
with an `eventId` on every event, a retry records nothing twice.
## Request Body
One to 25 events. Every event requires a `name`, a source-owned `eventId`, and
an `email` or an `externalId`. Email is required when creating a contact; an
external-ID-only row must resolve to an existing contact. `null` identifier
and timestamp values are treated as absent, so warehouse exports with null
columns import cleanly. Keys outside the documented fields are ignored - keep
row data inside `properties`, or it is silently discarded.
Subscriber delivery email address. Required when the event may create a new
contact.
Your app/customer/user ID for this subscriber. It can be used without email
only when it resolves to an existing contact.
Event name (e.g., `purchase_completed`, `saas.purchase`)
Event properties/metadata.
ISO 8601 timestamp of when the event actually happened. Defaults to now.
Classification is per contact: only when all rows for that contact are more
than an hour old is the group historical. Any recent row makes every event for
that contact live while preserving each supplied timestamp.
Your source-owned ID for this event, used as an idempotency key on both the
live and historical paths. Derive it from your source data (an order ID, a
warehouse row key) so re-running the import is safe.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events/imports" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"email": "one@example.com",
"name": "purchase_completed",
"eventId": "order_1001",
"occurredAt": "2026-08-01T12:00:00Z",
"properties": { "amount": 99.99 }
},
{
"email": "two@example.com",
"externalId": "user_42",
"name": "purchase_completed",
"eventId": "order_1002",
"occurredAt": "2026-08-02T09:30:00Z",
"properties": { "amount": 49.99 }
}
]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"total": 2,
"recorded": 2,
"duplicates": 0,
"failed": 0,
"sideEffectFailed": 0,
"subscribers": 2
}
```
```json 200 (partial failure) theme={null}
{
"success": false,
"total": 2,
"recorded": 1,
"duplicates": 0,
"failed": 1,
"sideEffectFailed": 0,
"subscribers": 2,
"failures": [{ "index": 1, "error": "Subscriber identity conflict" }],
"error": "Subscriber identity conflict"
}
```
```json 200 (post-write failure) theme={null}
{
"success": false,
"total": 1,
"recorded": 1,
"duplicates": 0,
"failed": 0,
"sideEffectFailed": 1,
"subscribers": 1,
"sideEffectFailures": [{ "index": 0, "stages": ["automation-triggering"] }],
"error": "One or more events were recorded, but downstream side effects failed."
}
```
```json 400 theme={null}
{
"success": false,
"error": "events[3] needs an email or an externalId."
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: automations:trigger"
}
```
```json 500 theme={null}
{ "success": false, "error": "Internal server error" }
```
## Counts
`total = recorded + duplicates + failed`. `duplicates` counts events whose
`eventId` was already recorded for that contact and event name, so no second
receipt was written. Live duplicate retries may idempotently re-attempt
downstream recovery. `sideEffectFailed` is orthogonal to receipt accounting:
it can accompany either a newly `recorded` row or a `duplicate` row whose
downstream work (or historical automation shielding) still failed.
`subscribers` is the number of distinct identities in the request.
## Throughput
At 25 events per request within the standard
[rate limits](/api-reference/introduction#rate-limiting), this endpoint
sustains up to 2,500 events per minute while keeping each synchronous request
bounded - use it instead of looping over
the single-subscriber endpoints when syncing events from a pipeline or
warehouse. See the
[data pipeline ingestion guide](/guides/data-pipeline-ingestion) for the full
recipe.
# Trigger Event
Source: https://docs.sequenzy.com/api-reference/subscribers/events/trigger
POST /api/v1/subscribers/events
Trigger an event for a subscriber
Trigger an event for a subscriber. Creates the subscriber and/or event definition if they don't exist. Events can trigger automations and apply sync rules.
## Request Body
Subscriber delivery email address. Required when creating a new subscriber.
Your app/customer/user ID for this subscriber. You can trigger events with
only `externalId` when the subscriber already exists.
Event name (e.g., `purchase_completed`, `saas.purchase`)
Event properties/metadata. Can contain any key-value data.
First name for the subscriber created or updated by this request. Omit to
leave an existing value unchanged.
Last name for the subscriber created or updated by this request. Omit to leave
an existing value unchanged.
Custom attributes to set on the subscriber.
ISO 8601 timestamp of when the event actually happened. Defaults to now. A
timestamp more than an hour in the past records the event as history - see
[Recording events that already
happened](#recording-events-that-already-happened).
Your own ID for this event, used as an idempotency key on both the live and
historical paths. Live retries return `duplicate: true`; historical retries
increment `duplicates`. See [Retrying the same
event](#retrying-the-same-event).
Attach the contact to one of your customers' organizations. See
[Accounts](../../../concepts/accounts). Pass the account's `externalId` as a
string, or an object with `externalId` plus optional `name`, `domain`, `role`
(`owner`, `admin` or `member`) and `attributes`. The account is created when
missing, and the event carries its data as `event.account.*`. Used only after
your workspace has Accounts on. Until then the field is ignored in any shape
and the response includes `accountIgnored`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"event": "purchase_completed",
"properties": {
"orderId": "order_12345",
"amount": 99.99,
"product": "Pro Plan",
"currency": "USD"
}
}'
```
## Retrying the Same Event
Producers retry. A queue redelivers, an automation tool re-runs a node, a
request times out after it was already handled. Pass your own `eventId` and
Sequenzy treats a repeat as the same occurrence:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"event": "saas.purchase",
"eventId": "order_12345",
"properties": { "plan": "pro", "amount": 99 }
}'
```
For a live event, if this contact already has an event with that ID and that name, nothing is
recorded, no sequences enroll, no sync rules apply, no webhooks fire, and the
response returns the existing event with `duplicate: true`:
```json theme={null}
{
"success": true,
"duplicate": true,
"subscriber": { "id": "sub_123", "email": "user@example.com" },
"event": { "id": "order_12345", "name": "saas.purchase" }
}
```
The ID is scoped to the contact and the event name, so keying `saas.purchase`
and `saas.refund` off the same order ID records both. Without an `eventId` a
repeat is recorded as a second occurrence - we cannot tell a retry from a
customer who genuinely bought twice.
This is best-effort, not exactly-once. Events are written to an analytics
store that ingests asynchronously, so two calls within about a second of each
other can both be recorded. It removes the duplicates that retries and
redeliveries actually produce; a producer that needs a strict guarantee should
keep its own ledger of what it has sent.
## Auto-Creation Behavior
This endpoint automatically creates resources if they don't exist:
| Resource | Behavior |
| - | - |
| **Subscriber** | Created if email doesn't exist - `active` status, or pending confirmation when double opt-in is enabled |
| **Event Definition** | Created if event name is new |
New subscribers created by this endpoint follow your workspace default lists
setting.
## What Happens When an Event is Triggered
1. **Event Recorded** - Stored in analytics for reporting
2. **Sync Rules Applied** - Tags automatically added/removed based on rules
3. **Automations Triggered** - Sequences with matching event triggers start
## Recording Events That Already Happened
When you migrate from another platform, or backfill history from your own
database, you want the events to carry the dates they really happened on.
Otherwise every imported purchase looks like it happened today and segments
like "purchased in the last 90 days" are wrong.
Pass `occurredAt` to say when the event happened:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"event": "saas.purchase",
"occurredAt": "2024-11-02T10:00:00Z",
"eventId": "inv_9182",
"properties": { "plan": "pro", "amount": 99 }
}'
```
An `occurredAt` more than an hour in the past is treated as **history**, and
the behavior changes deliberately:
| | Live event | Historical event |
| - | - | - |
| Stored with your timestamp | Yes | Yes |
| Counts for segments and the subscriber timeline | Yes | Yes |
| Starts or continues sequences | Yes | **No** |
| Applies sync rules | Yes | **No** |
| Resumes waiting steps, records goal conversions, sends webhooks | Yes | **No** |
| Moves the contact's signup date earlier | No | **Yes** |
| Response includes `historical: true` | No | Yes |
Historical events never run automations. This is what makes it safe to import
years of history in one afternoon: without it, every imported purchase would
enroll the contact in your purchase sequence and send a real email. Do not use
`occurredAt` to test a live automation - the side effects you are testing will
not run.
Timestamps within the last hour stay on the live path, so queue lag, retries and
clock skew behave exactly as before.
**You can set your flows up first.** Importing history is safe whatever order
you do it in - build and switch on every sequence you want, then bring your
data over. Historical events are also kept out of the calculations that
time-based flows use to pick people, so importing a year of purchases will not
satisfy a "bought 5 times in the last 30 days" flow, and importing contacts
will not make a "joined recently" flow treat them as new arrivals. They still
count for the segments and reports themselves - only the enrollment decision
ignores them.
**Signup dates.** An event at time T proves the contact existed at T, so a
historical event also moves the contact's signup date back to the earliest
event you import - never later. A contact created by the import gets that date
instead of today's, so date-added segments and growth reports do not read a
migration as a wave of new signups.
**Idempotency.** Pass `eventId` and a re-run writes nothing new, exactly as on
the live path, but the historical response reports skipped rows through its
`duplicates` count rather than the live path's `duplicate` flag and singular
`event` (see [Retrying the same event](#retrying-the-same-event)). Imports go
further: without an `eventId` Sequenzy derives a stable ID from the contact,
event name, timestamp and properties, so an identical historical event sent
twice still produces one row. Live events get no such fallback, because two
identical purchases seconds apart are a real thing customers do.
**Retention.** Events are kept for 5 years. A timestamp older than that is
rejected with a 400 rather than accepted and silently dropped. To represent a
contact who joined longer ago than that, set their signup date with
`createdAt` on [Create Subscriber](/api-reference/subscribers/create) instead -
subscriber records have no such limit.
## Using `properties` in Sequence Emails
If this event starts a sequence with `trigger: "event_received"`, Sequenzy stores the `properties` object on that sequence run and makes it available in later emails through `event.` merge tags.
Examples:
* `{{event.amount}}`
* `{{event.product}}`
* `{{event.order.id}}`
* `{{event.city|your area}}`
That means the second or third email in the same sequence can still use the original event payload without copying those values onto the subscriber profile.
## Built-in Events
These events have special behavior with default sync rules:
| Event | Tags Added | Tags Removed |
| - | - | - |
| `saas.purchase` | `customer` | `lead`, `past-due`, `cancelled`, `churned` |
| `saas.churn` | `churned` | `customer`, `cancelled`, `past-due` |
| `saas.cancelled` | `cancelled` | `customer` |
| `saas.payment_failed` | `past-due` | None |
| `saas.trial_started` | `trial` | `lead` |
| `saas.trial_cancelled` | `cancelled` | None |
## Double Opt-In
When the workspace has double opt-in enabled and the event creates a brand-new subscriber, the subscriber is stored pending confirmation, the confirmation email is queued, and sequences triggered by the event wait at their trigger step until the subscriber confirms. The response then includes an `optIn` object.
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"created": false
},
"event": {
"id": "evt_xyz789",
"name": "purchase_completed",
"definitionCreated": false
}
}
```
```json 200 (new subscriber, double opt-in enabled) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": null,
"created": true
},
"event": {
"id": "evt_xyz789",
"name": "purchase_completed",
"definitionCreated": false
},
"optIn": {
"required": true,
"emailQueued": true
}
}
```
```json 200 (event recorded, side effects failed) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"created": false
},
"event": {
"id": "evt_xyz789",
"name": "purchase_completed",
"definitionCreated": false
},
"sideEffectFailures": ["apply-sync-rules"]
}
```
```json 200 (account ignored, Accounts not turned on) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"created": false
},
"event": {
"id": "evt_xyz789",
"name": "project_created",
"definitionCreated": false
},
"accountIgnored": {
"reason": "accounts_not_enabled",
"message": "Accounts are not turned on for this workspace, so `account` was ignored. Create an account first (POST /api/v1/accounts) or turn Accounts on in Subscribers > Accounts."
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "`account` must be an external id string or an object with `externalId`."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Subscriber identity conflict: email and externalId point to different subscribers."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `subscriber.created` | `true` if subscriber was created by this request |
| `event.definitionCreated` | `true` if this event type was newly defined |
| `sideEffectFailures` | Present when the event was recorded but a follow-up stage failed (e.g. `apply-sync-rules`, `trigger-event-automations`). Treat as partial. |
| `account` | Present when `account` attached the contact: `externalId`, `role` and whether the account was `created`. |
| `accountIgnored` | Present when `account` was sent but Accounts is not on for the workspace. The event is still recorded. |
## Use Cases
### E-commerce Purchase
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"event": "saas.purchase",
"properties": {
"orderId": "ord_abc123",
"amount": 149.99,
"plan": "pro-annual",
"currency": "USD"
},
"customAttributes": {
"lastPurchaseDate": "2024-01-15"
}
}'
```
### Feature Activation
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"event": "feature_activated",
"properties": {
"feature": "ai-assistant",
"firstTime": true
}
}'
```
### Subscription Lifecycle
```bash theme={null}
# Trial started
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-d '{ "email": "user@example.com", "event": "saas.trial_started" }'
# Payment failed
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-d '{
"email": "user@example.com",
"event": "saas.payment_failed",
"properties": { "attemptCount": 2 }
}'
# Customer churned
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-d '{ "email": "user@example.com", "event": "saas.churn" }'
```
Events can trigger automation sequences. If you have a sequence set to start
when this event is received, it will begin automatically for the subscriber.
# Trigger Events (Bulk)
Source: https://docs.sequenzy.com/api-reference/subscribers/events/trigger-bulk
POST /api/v1/subscribers/events/bulk
Trigger multiple events for a subscriber in one request
Trigger multiple events for a subscriber in one request. Creates the subscriber and/or event definitions if they don't exist.
Events are processed independently. If one event fails, previously triggered
events are not rolled back, so retry an error response as a partial success.
## Request Body
Subscriber delivery email address. Required when creating a new subscriber.
Your app/customer/user ID for this subscriber. You can trigger events with
only `externalId` when the subscriber already exists.
Array of events to trigger. At most 500 per request.
Event name
Event properties/metadata
ISO 8601 timestamp of when this event actually happened. Defaults to now. See
[Importing history](#importing-history).
Your own ID for this event, making a re-run idempotent. On the live path an
event whose ID this contact already has for that name is skipped: nothing is
recorded, no side effects run, and the entry comes back with `duplicate:
true`. See [Retrying the same
event](/api-reference/subscribers/events/trigger#retrying-the-same-event).
First name for the subscriber created or updated by this request. Omit to
leave an existing value unchanged.
Last name for the subscriber created or updated by this request. Omit to leave
an existing value unchanged.
Custom attributes to set on the subscriber.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"events": [
{
"name": "page_viewed",
"properties": { "page": "/pricing", "duration": 45 }
},
{
"name": "feature_used",
"properties": { "feature": "export", "format": "csv" }
}
]
}'
```
## Auto-Creation Behavior
This endpoint automatically creates resources if they don't exist:
| Resource | Behavior |
| - | - |
| **Subscriber** | Created if email doesn't exist - `active` status, or pending confirmation when double opt-in is enabled |
| **Event Definitions** | Each event type created if new |
New subscribers created by this endpoint follow your workspace default lists
setting.
## Double Opt-In
When the workspace has double opt-in enabled and this request creates a brand-new subscriber, the subscriber is stored pending confirmation and a single confirmation email is queued for the whole request. All events are still recorded, and sequences triggered by them wait at their trigger step until the subscriber confirms. The response then includes an `optIn` object.
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"created": false
},
"events": [
{
"id": "evt_1",
"name": "page_viewed",
"definitionCreated": false
},
{
"id": "evt_2",
"name": "feature_used",
"definitionCreated": true
}
]
}
```
```json 200 (new subscriber, double opt-in enabled) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": null,
"created": true
},
"events": [
{
"id": "evt_1",
"name": "page_viewed",
"definitionCreated": false
},
{
"id": "evt_2",
"name": "feature_used",
"definitionCreated": true
}
],
"optIn": {
"required": true,
"emailQueued": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `subscriber.created` | `true` if subscriber was created by this request |
| `events[].definitionCreated` | `true` if this event type was newly defined |
| `events[].sideEffectFailures` | Present when that event was recorded but a follow-up stage failed (e.g. `apply-sync-rules`) |
| `optIn` | Present when the new subscriber requires double opt-in confirmation before becoming active |
## Use Cases
### Track User Session
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"events": [
{ "name": "session_started" },
{ "name": "page_viewed", "properties": { "page": "/dashboard" } },
{ "name": "feature_used", "properties": { "feature": "reports" } },
{ "name": "page_viewed", "properties": { "page": "/settings" } }
]
}'
```
### Record Funnel Progress
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"events": [
{ "name": "checkout_started", "properties": { "cartValue": 149.99 } },
{ "name": "payment_method_added", "properties": { "type": "card" } },
{ "name": "checkout_completed", "properties": { "orderId": "ord_123" } }
]
}'
```
Each event in the bulk request is processed independently. Sync rules and
automation triggers are evaluated for each event, and successful events are
not rolled back if another event in the same request fails.
## Importing History
Give each event an `occurredAt` and it is stored with the date it really
happened, so date-relative segments are correct straight after a migration
instead of treating your whole history as having happened today.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"events": [
{
"name": "saas.purchase",
"occurredAt": "2024-01-05T09:00:00Z",
"eventId": "inv_1001",
"properties": { "plan": "starter" }
},
{
"name": "saas.upgrade",
"occurredAt": "2025-02-20T14:30:00Z",
"eventId": "inv_1042",
"properties": { "from": "starter", "to": "pro" }
}
],
"customAttributes": {
"currentPlan": "pro",
"lifetimeValue": 599.00
}
}'
```
When **every** event in the batch is more than an hour old, the batch takes the
historical path: one idempotent write, no sequences, no sync rules, no waiting
steps, no goal conversions, no webhooks. The response carries
`historical: true` along with `inserted` and `duplicates` counts.
```json theme={null}
{
"success": true,
"historical": true,
"inserted": 2,
"duplicates": 0,
"subscriber": {
"id": "sub_123",
"email": "user@example.com",
"created": true
},
"events": [
{
"id": "inv_1001",
"name": "saas.purchase",
"occurredAt": "2024-01-05T09:00:00.000Z"
},
{
"id": "inv_1042",
"name": "saas.upgrade",
"occurredAt": "2025-02-20T14:30:00.000Z"
}
]
}
```
Historical events do not run automations. That is what makes a large import
safe - otherwise every imported purchase would enroll the contact and send a
real email. A batch that mixes recent and old events stays on the live path,
with each event keeping its own timestamp.
Re-running the same import writes nothing new: `eventId` makes each event
idempotent, and events without one are deduplicated by their content. Events
older than the 5-year retention window are rejected with a 400 rather than
accepted and silently dropped.
# Subscribers By External ID
Source: https://docs.sequenzy.com/api-reference/subscribers/external
GET /api/v1/subscribers/external
Get, update, or delete subscribers by your own external ID
Use external-ID routes when your app has a stable customer, account, or user ID. Prefer the query form because it supports external IDs that contain slashes.
For `GET`, email engagement stats and recent activity exclude detected email-security scanners and tracked brand assets by default.
Subscriber notes can also use external IDs with `GET` or `POST /api/v1/subscribers/external/notes?externalId={externalId}`.
## Request
Customer-owned external ID. Required for `GET`, `PATCH`, and `DELETE
/subscribers/external`.
Compatibility path form for external IDs that do not contain slashes.
Set to `true` on `GET` requests to include detected scanner, preview, and
tracked asset open/click events in `emailStats` and recent activity.
When machine engagement is included, open/click activity events include
`machine`, `engagementQuality`, and `classificationReasons` fields.
New subscriber email when updating.
New external ID when updating.
Updated first name.
Updated last name.
Updated phone number in E.164 or national format. Stored normalized to E.164
on the native phone field, not as a custom attribute. An invalid phone returns
`400 VALIDATION_ERROR`. Setting a phone does NOT subscribe the contact to SMS.
Changing the number without `smsConsent` resets SMS consent. `null` or `""`
clears the phone, except on a phone-only contact with no email.
ISO 3166-1 alpha-2 country used to read a national-format `phone`, such as
`IT` or `US`. Defaults to `US`. Send it only together with `phone`.
SMS marketing consent. `true` subscribes the contact with consent source `api`
and must only be sent with verified express written consent. `false`
unsubscribes the contact from SMS. When omitted, SMS status stays unchanged
unless `phone` changes, which resets consent because it belonged to the old
number. Consent is never inferred from phone presence. See [SMS &
MMS](/concepts/sms) for the full consent model.
Updated status: `active` or `unsubscribed`.
Replacement tag list.
Custom attributes to update. By default this replaces the existing public
custom-attribute map.
How to apply `customAttributes`: `replace` replaces the existing public
custom-attribute map, while `merge` overwrites only the provided keys and
retains unspecified existing keys.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "anna@example.com", "customAttributesStrategy": "merge", "customAttributes": {"plan": "pro"}}'
```
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "anna@example.com",
"externalId": "user_123",
"status": "active",
"tags": ["customer"],
"customAttributes": {
"plan": "pro"
}
}
}
```
```json 200 theme={null}
{
"success": true,
"deleted": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "externalId is required"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Another subscriber already uses that email or externalId"
}
```
Subscriber PATCH requests use the [same name validation as updates by email](/api-reference/subscribers/update):
first and last names support up to 255 Unicode characters. Overlong names return
`400` before changes are applied. Correct the name before retrying. This also
applies to the compatibility path route.
# Get Subscriber
Source: https://docs.sequenzy.com/api-reference/subscribers/get
GET /api/v1/subscribers/{email}
Get a subscriber by email address with notes, lists, sequence enrollments, and recent activity
Retrieve a subscriber by their email address. The response includes the base subscriber profile plus internal notes, list memberships, sequence enrollments, email engagement stats, and the most recent activity events. Open and click stats exclude detected email-security scanners and tracked brand assets by default.
You can also retrieve by external ID with `GET /api/v1/subscribers/external?externalId={externalId}`.
For commerce customers, `customAttributes` include Sequenzy's nightly
[predictive analytics](/concepts/predictive-analytics): `predictedLtv`,
`churnRisk`, `expectedNextOrderAt`, `avgDaysBetweenOrders`, and
`predictionConfidence`.
## Path Parameters
Subscriber email (URL encoded)
## Query Parameters
External ID to use with `/subscribers/external`. Use this route for IDs that
contain `/`.
Set to `true` to include detected scanner, preview, and tracked asset
open/click events in `emailStats` and recent activity.
When machine engagement is included, open/click activity events include
`machine`, `engagementQuality`, and `classificationReasons` fields so clients
can distinguish human activity from scanners and tracked assets.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/user%40sequenzy.com" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@sequenzy.com",
"externalId": "user_123",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"unsubscribedAt": null,
"emailProvider": "gmail",
"tags": ["customer", "newsletter"],
"customAttributes": { "plan": "pro" },
"notes": [
{
"id": "note_123",
"subscriberId": "sub_abc123",
"authorId": "user_123",
"body": "Asked about enterprise pricing",
"createdAt": "2026-06-28T10:00:00.000Z",
"updatedAt": "2026-06-28T10:00:00.000Z",
"author": {
"id": "user_123",
"name": "Ana",
"email": "ana@example.com"
}
}
],
"lists": [
{
"id": "list_vip",
"name": "VIP Customers",
"description": null,
"isPrivate": false,
"subscribedAt": "2024-01-10T09:00:00Z",
"unsubscribedAt": null
}
],
"sequenceEnrollments": [
{
"tokenId": "token_123",
"sequenceId": "seq_welcome",
"sequenceName": "Welcome Flow",
"sequenceStatus": "active",
"enrollmentStatus": "active",
"currentNodeId": "node_abc",
"currentNodeType": "action_email",
"currentNodeLabel": "Send welcome email",
"scheduledFor": null,
"enteredAt": "2024-01-10T09:05:00Z",
"updatedAt": "2024-01-10T09:05:00Z"
}
],
"emailStats": {
"sent": 12,
"delivered": 12,
"opened": 9,
"clicked": 3,
"bounced": 0,
"unsubscribed": 0,
"complained": 0
},
"activity": [
{
"id": "evt_1",
"eventType": "custom",
"eventTime": "2024-01-15T10:25:00Z",
"emailSendId": null,
"campaignId": null,
"clickedUrl": null,
"machine": false,
"engagementQuality": "human",
"classificationReasons": [],
"bounceType": null,
"eventName": "saas.purchase",
"properties": { "plan": "pro" }
}
],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "externalId is required"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Create Subscriber Import
Source: https://docs.sequenzy.com/api-reference/subscribers/import-create
POST /api/v1/subscribers/imports
Queue an asynchronous import of up to 5,000 full subscriber records
Use this endpoint for CRM migrations and first-list onboarding instead of
issuing one create request per contact. The request returns `202` immediately;
use the returned import ID with [Get Subscriber Import](/api-reference/subscribers/import-status).
## Permissions
The API key needs `subscribers:write`. Supplying `listIds` also requires
`lists:write`; requesting sequence enrollment or double-opt-in delivery requires
`automations:trigger`.
One to 5,000 records. Every record requires an `email` or a `phone`
(phone-only records import as SMS contacts) and may include `externalId`,
`firstName`, `lastName`, `phone`, `status`, `tags`, typed `customAttributes`,
`timezone`, and `createdAt`.
`active`, `unsubscribed`, or `bounced`, as the source platform reported it.
Unsubscribed and bounced records are stored with that status. They never
enroll in sequences and never receive a double opt-in confirmation, whatever
`enrollInSequences` and `optInMode` say. An `active` record never resubscribes
an existing contact who is unsubscribed or bounced, under any
`duplicateStrategy`. With `merge` or `overwrite`, an `unsubscribed` or
`bounced` record applies that status to an existing active contact; an
unsubscribe also marks their list memberships unsubscribed and cancels their
running sequences. With `skip`, existing contacts are left unchanged. Map
provider values such as AWeber's `Not Confirmed` or Mailchimp's `cleaned`
before sending: send `active` for contacts who never confirmed, as the
dashboard and CLI do, and `bounced` for cleaned addresses.
IANA time zone, such as `America/New_York`. Invalid or overlong time zones are
dropped for that row; they do not reject the whole import.
Attribute values may be strings, numbers, booleans, `null`, or arrays of
strings, numbers and booleans. Nested objects and arrays of objects are not
supported by this import endpoint.
Display name for this import, up to 255 characters, for example
`crm-export.csv`.
ISO 8601 date this contact originally signed up on the source platform.
Preserves their real history so date-relative segments are correct right after
the import. An existing contact's date only ever moves earlier. If any record
carries an unusable value the whole request is rejected with a `400` naming
the row, so you never end up with a half-imported list.
`skip`, `merge`, or `overwrite`.
Caller-owned key (1-255 characters; blank keys are rejected with a `400`) that
makes retrying this request safe. The key is scoped to the request content:
resending the same request returns the already-queued import with
`deduplicated: true` (as a `200`, or as a `202` while the import is still
starting up), while different content under the same key queues a new import
rather than silently discarding the corrected data. Use it whenever a timeout
or retry loop could resend the same import - pipelines should derive it from
the sync run (e.g. `nightly-sync-2026-08-25`). Note this differs from the
transactional send `Idempotency-Key` header, which rejects key reuse with
different content.
List IDs to apply to every imported subscriber. Every ID must belong to the
authenticated workspace.
`default` obeys the workspace setting, `confirmed` imports contacts whose
consent has already been verified, and `double_opt_in` queues confirmation
delivery. Keys without `automations:trigger` must use `confirmed` for active
consented contacts.
Enroll matching subscribers after import. Requires `automations:trigger`.
Two-letter country code used to normalize national-format phone numbers.
Apply the supplied SMS consent to imported phone numbers. Never infer this
from phone presence.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/imports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "copper-export.csv",
"duplicateStrategy": "merge",
"optInMode": "confirmed",
"listIds": ["list_abc123"],
"subscribers": [{
"email": "coach@example.com",
"externalId": "copper-23",
"firstName": "Ari",
"lastName": "Tan",
"phone": "81234567",
"tags": ["copper", "consented"],
"createdAt": "2019-03-14T09:30:00Z",
"customAttributes": { "source": "Copper", "sessions": 4 }
}]
}'
```
```json 202 theme={null}
{
"success": true,
"import": {
"id": "import_abc123",
"batchId": "batch_abc123",
"status": "running",
"totalRows": 1,
"emailCount": 1,
"duplicateRows": 0
},
"message": "Queued 1 subscriber for import."
}
```
```json 200 (idempotency key replay) theme={null}
{
"success": true,
"deduplicated": true,
"import": {
"id": "import_abc123",
"batchId": "idem-4f2a...",
"status": "completed",
"totalRows": 1
},
"message": "An import with this idempotency key was already queued."
}
```
```json 503 (concurrent idempotent enqueue) theme={null}
{
"success": false,
"retryable": true,
"importId": "import_abc123",
"error": "This subscriber import is still being queued by another request. Retry the same idempotency key shortly."
}
```
A `503` in this form means another request currently owns the short enqueue
lease; honor `Retry-After` and resend the identical request. It does not mean
the import failed, and the deterministic import and job IDs keep the retry
safe.
A completed import can contain row failures. Always inspect `failedCount` and
`failedReasons` before treating the migration as successful.
# Get Subscriber Import
Source: https://docs.sequenzy.com/api-reference/subscribers/import-status
GET /api/v1/subscribers/imports/{importId}
Get progress and row outcomes for a queued subscriber import
Returns a company-scoped import by its import ID or batch ID. The API key needs
`subscribers:read`.
Status `completed` means row processing has finished. Custom-attribute
updates sync separately, so attribute-based segment counts may still show
earlier values for roughly 30–35 seconds, or longer when background processing
is busy. Allow that sync to finish before rechecking
[Get Segment Count](/api-reference/segments/count).
Import ID or batch ID returned by the create endpoint.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/imports/import_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Reconciling an import
Every row an import excludes is explained: `skippedReasons` sums to
`skippedCount` and `failedReasons` sums to `failedCount`. On a completed import
`addedCount + updatedCount + skippedCount + failedCount` equals `totalRows`, so
you can prove nothing went missing without inspecting individual contacts.
Common values in `skippedReasons`:
| Reason | Meaning |
| - | - |
| `duplicate_csv_row` | Another submitted row carried the same email, phone, or external ID |
| `duplicate_strategy_skip` | The contact already existed and `duplicateStrategy` was `skip` |
| `merge_no_changes` | The contact already existed and the submitted values matched |
| `subscriber_action_returned_existing` | The contact already existed and nothing was written |
| `invalid_domain` | The email domain does not accept mail |
| `blocked_email` | The address is suppressed after prior delivery failures |
| `invalid_phone` | A phone-only row whose number could not be normalized |
| `missing_identity` | The row had no usable email or phone number |
| `canceled` | The import was canceled before the row was processed |
| `unreported_skip` | A skipped row could not be assigned a more specific reason |
```json 200 theme={null}
{
"success": true,
"import": {
"id": "import_abc123",
"batchId": "batch_abc123",
"fileName": "copper-export.csv",
"status": "completed",
"totalRows": 23,
"emailCount": 21,
"duplicateRows": 1,
"addedCount": 21,
"updatedCount": 0,
"skippedCount": 2,
"failedCount": 0,
"processedCount": 23,
"totalChunks": 1,
"completedChunks": 1,
"failedChunks": 0,
"skippedReasons": { "duplicate_csv_row": 1, "missing_identity": 1 },
"failedReasons": {},
"failedChunkReasons": {}
}
}
```
```json 401 theme={null}
{ "success": false, "error": "Unauthorized" }
```
```json 404 theme={null}
{ "success": false, "error": "Subscriber import not found" }
```
# List Subscribers
Source: https://docs.sequenzy.com/api-reference/subscribers/list
GET /api/v1/subscribers
List subscribers with pagination and filtering
List subscribers with stable pagination and optional filtering by status,
free-text query, tags, list, segment, attribute, email, or opt-out date.
Each subscriber carries `unsubscribedAt`, the date the contact opted out. Use
it rather than `updatedAt` to date an opt-out - `updatedAt` moves on any later
tag or attribute write. It is `null` unless the contact is currently
unsubscribed, so a contact who left one list but still receives email does not
carry a date, and it is `null` for contacts imported as already unsubscribed,
where no opt-out date exists.
Without an `attribute` filter, results are ordered by `createdAt` descending,
with subscriber ID used as a tie-breaker so page boundaries stay deterministic
when multiple subscribers have the same creation time.
With an `attribute` filter, results use ClickHouse-first cursor pagination and
are ordered by subscriber ID ascending. Attribute-filtered responses do not
include a total count; use `pagination.nextCursor` until it is `null`.
## Pulling a full audience
Every response includes `pagination.nextCursor` and `pagination.hasMore`. When
you need more than one page - a backup, an export, a migration - follow
`nextCursor` instead of incrementing `page`:
* Page numbers are computed with an offset, so rows can be skipped or repeated
if subscribers are created or deleted while you are paging. A cursor walks a
fixed sort key and is not affected.
* Cursor requests skip the total-count query, so `total` and `totalPages` come
back as `null`. Read the count from your first (cursorless) request.
* With `limit=1000`, a 10,000-subscriber audience takes ten requests.
```bash theme={null}
# First request: no cursor, returns the total
curl "https://api.sequenzy.com/api/v1/subscribers?status=active&limit=1000" \
-H "Authorization: Bearer YOUR_API_KEY"
# Then follow nextCursor until hasMore is false
curl "https://api.sequenzy.com/api/v1/subscribers?status=active&limit=1000&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Searching for one address
A `query` that is a whole email address is treated as an identity lookup: it
matches contacts whose email is that address (case-insensitively), or whose
external ID is that address, and nothing else. Anything shorter - `gmail.com`,
`@example.com`, `ana` - stays a substring search across email, external ID,
first name, last name, and tags.
The distinction matters on a large audience. A substring search has to examine
every contact you have; an address lookup does not. If you only need one
contact and you already know the address, prefer `GET /v1/subscribers/{email}`,
which returns the full contact record directly.
## Query Parameters
Page number. Cannot be combined with `cursor`.
Items per page (max 1000)
Cursor returned as `pagination.nextCursor`. Cannot be combined with `page`.
Attribute-filtered requests return their own cursor, which is not
interchangeable with the default-ordering cursor.
Pass `false` to skip the total-count query on page-numbered requests. Cursor
requests always skip it.
Filter by status: `active`, `unsubscribed`, `bounced`, or `all`. Use `all` to
disable status filtering.
Free-text search across email, first name, last name, and tags. A term that is
a whole email address is matched exactly against the contact's email or
external ID instead - see [Searching for one
address](#searching-for-one-address).
Legacy alias for `query`. Follows the same matching rules.
Comma-separated tag names. A subscriber must have all provided tags.
Filter by a custom attribute using `attributeName:value` syntax, such as
`plan:pro` or `mrr:50`. Use `attributeName:` only with empty-check operators.
Attribute operator. Direct cursor pagination supports inclusion operators:
`is`, `contains`, `gt`, `gte`, `lt`, `lte`, and `is_not_empty`. For exclusion
operators such as `is_not`, `not_contains`, or `is_empty`, create a saved
segment and pass `segmentId`.
Subscriber list ID or exact list name. The API tries ID first, then exact
name.
Filter by subscriber list ID.
Filter by exact subscriber list name when the ID is not known.
Filter by an existing segment ID. A segment whose references to other segments
exceed the [segment reference limits](/api-reference/segments/count) returns
`400` with code `SEGMENT_REFERENCE_DEPTH_EXCEEDED` or
`SEGMENT_REFERENCE_COUNT_EXCEEDED`.
Only return contacts whose `unsubscribedAt` is on or after this ISO 8601 date
or datetime. Bare dates use UTC midnight; datetimes must include `Z` or an
explicit offset. Contacts with no opt-out date are excluded, which includes
active contacts and imports with no known date.
Only return contacts whose `unsubscribedAt` is on or before this ISO 8601 date
or datetime. Bare dates use UTC midnight; datetimes must include `Z` or an
explicit offset.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers?page=1&limit=100&status=active&list=Master%20List" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Reconcile a window of opt-outs against another system:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers?status=unsubscribed&unsubscribedAfter=2026-07-01&unsubscribedBefore=2026-08-01&limit=1000" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers?attribute=mrr%3A50&attributeOperator=gte" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers?attribute=mrr%3A50&attributeOperator=gte&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"subscribers": [
{
"id": "sub_abc123",
"email": "user@sequenzy.com",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"unsubscribedAt": null,
"emailProvider": "gmail",
"tags": ["customer", "newsletter"],
"customAttributes": { "plan": "pro" },
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 1000,
"total": 150,
"totalPages": 1,
"nextCursor": null,
"hasMore": false,
"orderBy": "created_at_desc_id_desc"
}
}
```
```json 200 theme={null}
{
"success": true,
"subscribers": [
{
"id": "sub_abc123",
"email": "user@sequenzy.com",
"firstName": "John",
"lastName": "Doe",
"status": "active",
"emailProvider": "gmail",
"tags": ["customer", "newsletter"],
"customAttributes": { "mrr": 99 },
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 100,
"total": null,
"totalPages": null,
"nextCursor": "NEXT_CURSOR",
"hasMore": true,
"orderBy": "subscriber_id_asc"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 400 theme={null}
{
"success": false,
"error": "status must be one of active, unsubscribed, bounced, or all"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Attribute filters must include a value after \"attributeName:\"."
}
```
```json 400 theme={null}
{
"success": false,
"error": "cursor cannot be combined with page. Keep following nextCursor instead of incrementing page."
}
```
```json 404 theme={null}
{
"success": false,
"error": "Segment not found"
}
```
```json 404 theme={null}
{
"success": false,
"error": "List not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Get Account Info
Source: https://docs.sequenzy.com/api-reference/subscribers/me
GET /api/v1/subscribers/me
Returns account information for the authenticated API key
Returns account information for the authenticated API key. This endpoint is primarily used for connection labels and workspace-safe credential validation in integrations.
## Request
No parameters required. Authentication is handled via the API key.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/me" \
-H "x-api-key: YOUR_API_KEY"
```
## Responses
```json 200 Success theme={null}
{
"success": true,
"account": {
"companyId": "company_abc123",
"companyName": "Acme Inc",
"apiKeyType": "company"
}
}
```
```json 401 Unauthorized theme={null}
{
"success": false,
"error": "Invalid or missing API key"
}
```
```json 403 Forbidden theme={null}
{
"success": false,
"error": "Company not found"
}
```
# Subscriber Notes
Source: https://docs.sequenzy.com/api-reference/subscribers/notes
GET /api/v1/subscribers/{email}/notes
List, create, or delete internal notes for a subscriber
Internal subscriber notes are visible to your team and are not sent to subscribers.
You can manage notes by email or by external ID. Use the external-ID query route for IDs that contain `/`.
## Request
Subscriber email for `/subscribers/{email}/notes`.
External ID for `/subscribers/external/notes?externalId={externalId}`.
Note body. Maximum length: 5000 characters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers/user%40sequenzy.com/notes" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/user%40sequenzy.com/notes" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Asked about enterprise pricing"}'
```
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/subscribers/notes/note_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"notes": [
{
"id": "note_123",
"subscriberId": "sub_abc123",
"authorId": "user_123",
"body": "Asked about enterprise pricing",
"createdAt": "2026-06-28T10:00:00.000Z",
"updatedAt": "2026-06-28T10:00:00.000Z",
"author": {
"id": "user_123",
"name": "Ana",
"email": "ana@example.com"
}
}
]
}
```
```json 200 theme={null}
{
"success": true,
"note": {
"id": "note_123",
"subscriberId": "sub_abc123",
"authorId": "user_123",
"body": "Asked about enterprise pricing",
"createdAt": "2026-06-28T10:00:00.000Z",
"updatedAt": "2026-06-28T10:00:00.000Z",
"author": {
"id": "user_123",
"name": "Ana",
"email": "ana@example.com"
}
}
}
```
```json 200 theme={null}
{
"success": true,
"deleted": true,
"id": "note_123",
"subscriberId": "sub_abc123"
}
```
```json 400 theme={null}
{
"success": false,
"error": "body is required"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
# Cancel a Subscriber Operation
Source: https://docs.sequenzy.com/api-reference/subscribers/operation-cancel
POST /api/v1/subscribers/operations/{id}/cancel
Stop future work while retaining completed changes
Stop future work while retaining completed changes. See [background contact tasks](../../concepts/subscribers#background-tagging) for selection, retry and cancellation behavior.
Requires subscribers:read. Tagging and cancelling a tagging task also require subscribers:tag; creating tag definitions requires tags:write; triggering automations requires automations:trigger. Personal keys retain current company role restrictions. Company keys retain company-scoped authority. Workers recheck authority on every page.
Cancellation returns `200` with the terminal operation. Applied tags remain. A contact being updated when cancellation wins is recorded in `failures` with an uncertain outcome; check it before starting another task. Repeating cancellation returns the same terminal state. A busy cancellation race returns `409` and can be retried.
Missing authentication returns `401`; missing scopes or company access return `403`. An operation outside the selected company is reported as `404`, as is an operation whose retained record no longer exists.
# Get a Subscriber Operation
Source: https://docs.sequenzy.com/api-reference/subscribers/operation-get
GET /api/v1/subscribers/operations/{id}
Read tagging progress and outcomes
Read tagging progress and outcomes. See [background contact tasks](../../concepts/subscribers#background-tagging) for selection, retry and cancellation behavior.
Requires subscribers:read. Tagging and cancelling a tagging task also require subscribers:tag; creating tag definitions requires tags:write; triggering automations requires automations:trigger. Personal keys retain current company role restrictions. Company keys retain company-scoped authority. Workers recheck authority on every page.
Returns `200` with `success` and `operation`. Inspect `processed`, `succeeded`, `failed` and the bounded `failures` sample. Completed tagging can still include failed contacts.
Missing authentication returns `401`; missing scopes or company access return `403`. An operation outside the selected company is reported as `404`, as is an operation whose retained record no longer exists.
# List Subscriber Operations
Source: https://docs.sequenzy.com/api-reference/subscribers/operation-list
GET /api/v1/subscribers/operations
Read recent background tagging tasks
Read recent background tagging tasks. See [background contact tasks](../../concepts/subscribers#background-tagging) for selection, retry and cancellation behavior.
Requires subscribers:read. Tagging and cancelling a tagging task also require subscribers:tag; creating tag definitions requires tags:write; triggering automations requires automations:trigger. Personal keys retain current company role restrictions. Company keys retain company-scoped authority. Workers recheck authority on every page.
Returns `200` with `success` and up to twenty recent retained `operations`, newest first. Poll while a task is queued or running.
Missing authentication returns `401`; missing scopes or company access return `403`. An operation outside the selected company is reported as `404`, as is an operation whose retained record no longer exists.
# Start a Subscriber Operation
Source: https://docs.sequenzy.com/api-reference/subscribers/operation-start
POST /api/v1/subscribers/operations
Add tags to a filtered audience in the background
Add tags to a filtered audience in the background. See [background contact tasks](../../concepts/subscribers#background-tagging) for selection, retry and cancellation behavior.
Requires subscribers:read. Tagging and cancelling a tagging task also require subscribers:tag; creating tag definitions requires tags:write; triggering automations requires automations:trigger. Personal keys retain current company role restrictions. Company keys retain company-scoped authority. Workers recheck authority on every page.
Send `kind: "add_tags"`, a unique `requestKey` and one to twenty-five `tags`. Omit `audience` for all contacts, or supply the same filters as Contacts. `triggerAutomations` defaults to false through API, CLI and MCP; the Contacts tagging action explicitly enables it. Reuse the same key and settings after an interrupted response. A changed request returns `409`.
```json theme={null}
{
"kind": "add_tags",
"requestKey": "contacts-september-2026",
"tags": ["vip"],
"audience": { "activeOnly": true }
}
```
A `202` response contains `success` and `operation`, including its ID and current state. Invalid inputs return `400`; an operation that cannot be created returns `503`, which you can retry with the same request key.
A `400` names the failing fields. `error` summarizes up to three problems, and `issues` lists up to ten, each with its field `path`, a `message` and, for fixed choices, the `allowedValues`.
```json theme={null}
{
"success": false,
"error": "Invalid subscriber operation: kind: must be \"add_tags\"",
"issues": [
{
"path": "kind",
"message": "must be \"add_tags\"",
"allowedValues": ["add_tags"]
}
]
}
```
Missing authentication returns `401`; missing scopes or company access return `403`. An operation outside the selected company is reported as `404`, as is an operation whose retained record no longer exists.
# Add Tag
Source: https://docs.sequenzy.com/api-reference/subscribers/tags/add
POST /api/v1/subscribers/tags
Add a tag to a subscriber
Add a single tag to a subscriber. Creates the subscriber and/or tag if they don't exist.
## Request Body
Subscriber delivery email address. Required when creating a new subscriber.
Your app/customer/user ID for this subscriber. You can tag with only
`externalId` when the subscriber already exists.
Tag name to add. Will be normalized (lowercase, hyphens).
First name for the subscriber created or updated by this request. Omit to
leave an existing value unchanged.
Last name for the subscriber created or updated by this request. Omit to leave
an existing value unchanged.
Custom attributes to set on the subscriber.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"tag": "customer"
}'
```
## Auto-Creation Behavior
This endpoint automatically creates resources if they don't exist:
| Resource | Behavior |
| - | - |
| **Subscriber** | Created if email doesn't exist - `active` status, or pending confirmation when double opt-in is enabled |
| **Tag** | Created and normalized if tag name doesn't exist |
This makes integration seamless: you don't need to pre-create anything.
## Double Opt-In
When the workspace has double opt-in enabled and this request creates a brand-new subscriber, the subscriber is stored pending confirmation and the confirmation email is queued. The tag is still applied immediately, but tag automations wait at their trigger step until the subscriber confirms. The response then includes an `optIn` object.
## Tag Normalization
Tags are lowercased and runs of whitespace become one hyphen. Underscores and
punctuation are preserved:
```
"Pro Customer" → "pro-customer"
"VIP_User" → "vip_user"
"Newsletter!" → "newsletter!"
```
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"tags": ["customer"],
"created": false
},
"tag": {
"id": "tag_xyz789",
"name": "customer",
"created": false
}
}
```
```json 200 (new subscriber, double opt-in enabled) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": null,
"tags": ["customer"],
"created": true
},
"tag": {
"id": "tag_xyz789",
"name": "customer",
"created": false
},
"optIn": {
"required": true,
"emailQueued": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Subscriber identity conflict: email and externalId point to different subscribers."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `subscriber.created` | `true` if subscriber was created by this request |
| `tag.created` | `true` if tag definition was created by this request |
| `optIn` | Present when the new subscriber requires double opt-in confirmation before becoming active |
## Use Cases
### Track Customer Status
```bash theme={null}
# When user purchases
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "buyer@example.com",
"tag": "customer",
"customAttributes": {
"purchaseDate": "2024-01-15",
"plan": "pro"
}
}'
```
### Segment by Interest
```bash theme={null}
# When user shows interest in a topic
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "reader@example.com",
"tag": "interested-ai"
}'
```
Adding a tag can trigger automations. If you have a sequence set to start when
the tag is added, it will begin automatically.
# Add Tags (Bulk)
Source: https://docs.sequenzy.com/api-reference/subscribers/tags/add-bulk
POST /api/v1/subscribers/tags/bulk
Add multiple tags to a subscriber in one request
Add multiple tags to a subscriber in one request. Creates the subscriber and/or tags if they don't exist.
## Request Body
Subscriber delivery email address. Required when creating a new subscriber.
Your app/customer/user ID for this subscriber. You can add tags with only
`externalId` when the subscriber already exists.
Array of tag names to add
First name for the subscriber created or updated by this request. Omit to
leave an existing value unchanged.
Last name for the subscriber created or updated by this request. Omit to leave
an existing value unchanged.
Custom attributes to set on the subscriber.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"tags": ["customer", "newsletter", "pro-plan"]
}'
```
## Auto-Creation Behavior
This endpoint automatically creates resources if they don't exist:
| Resource | Behavior |
| - | - |
| **Subscriber** | Created if email doesn't exist - `active` status, or pending confirmation when double opt-in is enabled |
| **Tags** | Each tag created and normalized if it doesn't exist |
## Double Opt-In
When the workspace has double opt-in enabled and this request creates a brand-new subscriber, the subscriber is stored pending confirmation and the confirmation email is queued. The tags are still applied immediately, but tag automations wait at their trigger step until the subscriber confirms. The response then includes an `optIn` object.
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"tags": ["customer", "newsletter", "pro-plan"],
"created": false
},
"tags": {
"added": ["customer", "newsletter", "pro-plan"],
"created": ["pro-plan"]
}
}
```
```json 200 (new subscriber, double opt-in enabled) theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": null,
"tags": ["customer", "newsletter"],
"created": true
},
"tags": {
"added": ["customer", "newsletter"],
"created": []
},
"optIn": {
"required": true,
"emailQueued": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `subscriber.created` | `true` if subscriber was created by this request |
| `tags.added` | All tags now on the subscriber from this request |
| `tags.created` | Tags that were newly created (didn't exist before) |
| `optIn` | Present when the new subscriber requires double opt-in confirmation before becoming active |
## Use Cases
### New Customer Onboarding
```bash theme={null}
# Tag with multiple attributes at once
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new-customer@example.com",
"tags": ["customer", "onboarding", "2024-cohort", "source-organic"],
"customAttributes": {
"plan": "pro",
"signupDate": "2024-01-15"
}
}'
```
### Update Segments
```bash theme={null}
# Batch update subscriber segments
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"tags": ["enterprise", "high-value", "priority-support"]
}'
```
Tags are normalized (lowercase, hyphens) before being added. The
`tags.created` array shows which tag definitions were newly created by this
request.
# Add Tags to Many Subscribers
Source: https://docs.sequenzy.com/api-reference/subscribers/tags/bulk-add
POST /api/v1/subscribers/bulk/tags/add
Add tags to up to 500 existing subscribers in one request
Add one or more tags to up to 500 existing subscribers in a single request. This endpoint is built for reconciling historical or derived tags, so it behaves differently from the single-subscriber [Add Tag](/api-reference/subscribers/tags/add) endpoint in two important ways:
* **It never creates contacts.** Identifiers that do not match an existing subscriber come back in `notFound`, so a typo in a 3,000-row reconciliation cannot create 3,000 phantom contacts.
* **Tag automations are skipped by default.** Set `triggerAutomations` to `true` only when you actually want these contacts enrolled in `tag_added` sequences.
Identify subscribers by `emails`, `externalIds`, `subscriberIds`, or any combination. All identifier lists together must total at most 500 entries; split larger backlogs into batches.
## Permissions
This endpoint needs `subscribers:tag`, which the Safer agent access preset
includes. Adding a tag name that does not exist in the workspace yet also
creates its definition, so that case additionally requires `tags:write`, which
Safer agent access also includes; applying a tag that already exists does not. Keys that still carry the broader
`subscribers:write` permission satisfy `subscribers:tag` automatically.
## Request
Tag names to add to every matched subscriber, up to 25 per request. Names are
normalized (lowercase, hyphens) the same way as everywhere else in Sequenzy.
Subscriber emails to tag.
Your app/customer/user IDs to tag.
Sequenzy subscriber IDs to tag.
Whether `tag_added` sequences may enroll these contacts. Defaults to `false`.
Requires the `automations:trigger` scope.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/bulk/tags/add" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tags": ["derived-churn-risk"],
"emails": ["one@example.com", "two@example.com"]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"tags": ["derived-churn-risk"],
"requested": 3,
"matched": 2,
"updated": 2,
"unchanged": 0,
"failed": 0,
"notFound": {
"emails": ["ghost@example.com"],
"externalIds": [],
"subscriberIds": []
},
"failures": [],
"triggeredAutomations": false,
"message": "Tagged 2 subscribers (0 already in the target state, 0 failed)."
}
```
```json 400 theme={null}
{
"success": false,
"error": "A maximum of 500 subscribers can be tagged per request. Split the request into smaller batches."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "triggerAutomations requires the automations:trigger scope. Omit it to reconcile tags without enrolling contacts in sequences."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `requested` | Identifiers you supplied |
| `matched` | Existing subscribers those identifiers resolved to |
| `updated` | Subscribers whose tags actually changed |
| `unchanged` | Subscribers that already carried every tag |
| `failed` | Subscribers whose update failed; details in `failures` (max 50) |
| `notFound` | Identifiers that did not resolve, grouped by kind. Not created. |
Use [Add Tag](/api-reference/subscribers/tags/add) or [Add Tags
(Bulk)](/api-reference/subscribers/tags/add-bulk) when you want a single
contact created on the fly. Use this endpoint when the contacts already exist
and you are reconciling many of them at once.
# Remove Tags from Many Subscribers
Source: https://docs.sequenzy.com/api-reference/subscribers/tags/bulk-remove
POST /api/v1/subscribers/bulk/tags/remove
Remove tags from up to 500 existing subscribers in one request
Remove one or more tags from up to 500 existing subscribers in a single request. Use this to roll back or reconcile a derived-tag backfill.
Like [Add Tags to Many Subscribers](/api-reference/subscribers/tags/bulk-add), this endpoint never creates contacts: identifiers that do not match an existing subscriber come back in `notFound`.
Identify subscribers by `emails`, `externalIds`, `subscriberIds`, or any combination. All identifier lists together must total at most 500 entries.
## Permissions
This endpoint needs `subscribers:tag`, which the Safer agent access preset
includes. Keys that still carry the broader `subscribers:write` permission
satisfy it automatically.
## Request
Tag names to remove from every matched subscriber, up to 25 per request.
Subscriber emails to untag.
Your app/customer/user IDs to untag.
Sequenzy subscriber IDs to untag.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/bulk/tags/remove" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tags": ["derived-churn-risk"],
"subscriberIds": ["sub_abc123", "sub_def456"]
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"tags": ["derived-churn-risk"],
"requested": 2,
"matched": 2,
"updated": 1,
"unchanged": 1,
"failed": 0,
"notFound": {
"emails": [],
"externalIds": [],
"subscriberIds": []
},
"failures": [],
"message": "Untagged 1 subscriber (1 already in the target state, 0 failed)."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide emails, externalIds, or subscriberIds"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
Removing a tag does not trigger sequences. Sequenzy does not currently support
`tag_removed` automation triggers.
# Remove Tag
Source: https://docs.sequenzy.com/api-reference/subscribers/tags/remove
POST /api/v1/subscribers/tags/remove
Remove a tag from a subscriber
Remove a tag from a subscriber. If the subscriber doesn't exist, they will be created (without the tag).
## Request Body
Subscriber delivery email address. Required when creating a new subscriber.
Your app/customer/user ID for this subscriber. You can remove tags with only
`externalId` when the subscriber already exists.
Tag name to remove
First name (used if creating new subscriber)
Last name (used if creating new subscriber)
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/remove" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"externalId": "user_123",
"tag": "premium"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@example.com",
"externalId": "user_123",
"tags": ["newsletter"],
"created": false
},
"tag": {
"name": "premium",
"removed": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
## Response Fields
| Field | Description |
| - | - |
| `subscriber.created` | `true` if subscriber was created by this request |
| `tag.removed` | `true` if the tag was removed from the subscriber |
## Use Cases
### Downgrade Customer Status
```bash theme={null}
# When customer cancels subscription
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/remove" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"tag": "premium"
}'
```
### Remove Interest Tag
```bash theme={null}
# When user unsubscribes from a topic
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/remove" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "reader@example.com",
"tag": "interested-ai"
}'
```
If the subscriber doesn't have the specified tag, the request will still
succeed with `tag.removed: false`.
# Update Subscriber
Source: https://docs.sequenzy.com/api-reference/subscribers/update
PATCH /api/v1/subscribers/{email}
Update a subscriber's details or fully unsubscribe them
Update a subscriber's first name, last name, status, tags, or custom attributes.
When you set `status` to `unsubscribed`, Sequenzy runs the full unsubscribe workflow:
it unsubscribes the contact from all lists and cancels their active sequence enrollments.
You can also update by external ID with `PATCH /api/v1/subscribers/external?externalId={externalId}`.
## Path Parameters
Subscriber email (URL encoded)
## Query Parameters
External ID to use with `/subscribers/external`. Use this route for IDs that
contain `/`.
## Request Body
First name, up to 255 Unicode characters. Omit to keep unchanged, or send `""`
to clear. `null` is not accepted.
New delivery email address. This is most useful on the external ID route.
New customer-owned external ID. The update fails with `409` if another
subscriber owns it.
Last name, with the same limit and clear behavior as `firstName`.
IANA timezone identifier such as `America/New_York`, stored as a native
profile field. Enables recipient-local campaign delivery. An invalid
identifier returns `400 VALIDATION_ERROR`; `null` clears the stored value.
Phone number in E.164 format (`+15551234567`) or national format. Stored
normalized to E.164 on the native phone field, not as a custom attribute. An
invalid phone returns `400 VALIDATION_ERROR`. Setting a phone does NOT
subscribe the contact to SMS - use `smsConsent` for that, and note that
changing the number resets SMS consent unless `smsConsent` is sent in the same
request. `null` or `""` clears the phone, except on a phone-only (SMS) contact
* clearing the only identity it has returns `400 VALIDATION_ERROR`.
ISO 3166-1 alpha-2 country used to read a national-format `phone`, such as
`IT` or `US`. Defaults to `US`. This is a parsing hint only: the stored
`phoneCountry` always comes from the parsed number, so it can never disagree
with the phone it belongs to. Sending it without `phone` returns `400
VALIDATION_ERROR`.
SMS marketing consent. `true` sets the subscriber's SMS status to `subscribed`
with consent source `api` - only send `true` when you have express written
consent. `false` sets it to `unsubscribed`. When omitted, SMS status stays
unchanged unless `phone` changes, which resets consent because it belonged to
the old number. Consent is never inferred from phone presence. See [SMS &
MMS](/concepts/sms) for the full consent model.
Status: `active`, `unsubscribed`, or `bounced`. Setting `active` resubscribes
the contact. Setting `unsubscribed` performs a full global unsubscribe.
Replace all tags with this array
Custom attributes to update. By default this replaces the existing public
custom-attribute map.
How to apply `customAttributes`: `replace` replaces the existing public
custom-attribute map, while `merge` overwrites only the provided keys and
retains unspecified existing keys.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/subscribers/user%40sequenzy.com" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"status": "active",
"phone": "+15551234567",
"smsConsent": true,
"tags": ["customer", "vip"]
}'
```
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/subscribers/external?externalId=user_123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new-user@example.com",
"lastName": "Updated",
"customAttributesStrategy": "merge",
"customAttributes": {
"plan": "pro"
}
}'
```
With `customAttributesStrategy: "merge"`, a subscriber whose existing
`customAttributes` are
`{"arr":["1","2"],"letter":"X","val":true,"other":"abc"}` and whose PATCH
payload is
`{"customAttributesStrategy":"merge","customAttributes":{"arr":["3"],"letter":"Z","number":3}}`
ends with `{"arr":["3"],"letter":"Z","number":3,"val":true,"other":"abc"}`.
Names longer than 255 Unicode characters return `400` with a field-specific
error before any identity, status, tag, or attribute changes. Correct the name
before retrying. For compatibility, excess trailing ASCII spaces remain accepted
and are truncated by the database; names are otherwise never truncated.
## Responses
```json 200 theme={null}
{
"success": true,
"subscriber": {
"id": "sub_abc123",
"email": "user@sequenzy.com",
"externalId": "user_123",
"firstName": "Jane",
"lastName": "Doe",
"phone": "+15551234567",
"phoneCountry": "US",
"smsStatus": "subscribed",
"status": "active",
"tags": ["customer", "vip"],
"customAttributes": { "plan": "pro" },
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "externalId is required"
}
```
```json 400 Invalid name theme={null}
{
"success": false,
"error": "lastName must be at most 255 characters"
}
```
```json 400 Invalid phone theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid phone number. Provide a valid phone in E.164 format, e.g. +15551234567, or US national format."
}
}
```
```json 422 theme={null}
{
"type": "validation",
"on": "body",
"property": "/status",
"message": "Expected union value"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Subscriber not found"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "Subscriber identity conflict: target email belongs to a different subscriber."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Get Recipient Suppression
Source: https://docs.sequenzy.com/api-reference/suppressions/get
GET /api/v1/suppressions/{email}
Check suppression for one exact recipient address.
Checks one exact recipient against Sequenzy's bounce and complaint
safeguards. This endpoint does not list unrelated recipients.
Sequenzy suppresses recipients solely from its own bounce records. The email
provider's account-level suppression list is not consulted.
## Request
Exact recipient email address. URL-encode the value when constructing the
request URL.
Accepted and ignored. It previously limited a provider-side suppression
lookup, which no longer happens.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/suppressions/user%40example.com" \
-H "Authorization: Bearer API_KEY"
```
## Response
```json 200 theme={null}
{
"success": true,
"suppression": {
"email": "user@example.com",
"knownRecipient": true,
"suppressed": false,
"local": {
"suppressed": false,
"suppressionType": null,
"reason": null,
"source": null,
"suppressionId": null,
"scope": null,
"delistable": false
},
"ses": {
"regionsChecked": [],
"entries": []
}
}
}
```
`local.scope` describes how far a local suppression reaches: `global` when the
SMTP evidence conclusively identified an invalid destination and every
workspace is blocked, `company` when the block is contained to your workspace
(an ambiguous hard bounce, repeated delivery failures of your own sends, or a
spam complaint against your email).
Global invalid-recipient suppressions are protected; only company-scoped
soft-bounce escalations can be removed. It is
`null` when the address is not locally suppressed. Use
[List Suppressed Recipients](/api-reference/suppressions/list) to see every
suppressed address at once.
`local.suppressionType` uses the same stable values as the list:
`invalid_recipient`, `unknown_hard_bounce`, `soft_bounce_escalation`, or
`complaint`.
`local.delistable` is `true` only for `soft_bounce_escalation`.
`knownRecipient` indicates whether the address appears in this company's
subscriber or email-send history. Unknown addresses return an empty result so
the endpoint cannot be used to probe suppressions belonging to another company.
The `ses` object is deprecated and always returns `regionsChecked: []` and
`entries: []`. It is retained so existing integrations keep parsing. Read
`suppressed` and `local` instead.
# List Suppressed Recipients
Source: https://docs.sequenzy.com/api-reference/suppressions/list
GET /api/v1/suppressions
List the recipients your workspace cannot currently reach.
Lists every recipient your workspace is currently blocked from mailing, newest
suppression first. Suppressed recipients are silently skipped by campaigns,
sequences, and transactional sends, so this is the first place to look when
someone reports missing email.
Global rows are returned only when the address is already associated with your
workspace through recipient history; the shared platform list is never exposed.
Use
[Get Recipient Suppression](/api-reference/suppressions/get) for a full check of
one address, and
[Remove Recipient Suppression](/api-reference/suppressions/remove) to clear a
workspace soft-bounce escalation.
## Entry types
New suppressions persist their stable type in `bounced_email.suppression_type`.
Historical complaint sends are still included as a compatibility fallback, so
older complaint entries can report `source: "email_send_complaint"` while new
ones report `source: "bounced_email"`.
* `suppressionType: "invalid_recipient"` - the mail provider reported a
destination-address failure that conclusively identifies an invalid address.
Its scope is `global`, it is shown as protected to every workspace associated
with the address, and it cannot be removed by a workspace.
* `suppressionType: "unknown_hard_bounce"` - the provider reported a permanent
or undetermined failure, but its SMTP evidence did not prove the inbox was
invalid. Its scope is `company` and it is protected.
* `suppressionType: "soft_bounce_escalation"` - repeated delivery failures of
your own sends escalated the address. Its scope is `company`, and it can be
removed by an administrator for that workspace.
* `reason: "complaint"` - the recipient reported one of your emails as spam.
Its `suppressionType` is `complaint`, its scope is `company`, and it is
protected: `delistable` is `false` and removal is refused.
Only addresses that are actually blocked appear. An address partway through the
repeated-failure count is still deliverable and is not listed until it
escalates.
## Request
Case-insensitive substring filter on the recipient email address.
1-based page number. Defaults to `1`.
Entries per page. Defaults to `25`, maximum `100`.
Field to order by: `suppressedAt` (default), `email`, or `status`. Use
`status` to list the removable workspace escalations before the protected
suppressions. An unrecognized value falls back to `suppressedAt` instead of
failing the request - read `sortBy` in the response to confirm what was
applied.
Sort direction, `asc` or `desc`. Defaults to `desc` for `suppressedAt` and
`status`, `asc` for `email`.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/suppressions?limit=25&sort=status&order=desc" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"suppressions": [
{
"email": "user@example.com",
"suppressionType": "invalid_recipient",
"reason": "bounced",
"scope": "global",
"source": "bounced_email",
"bounceType": "Permanent",
"bounceSubType": "General",
"bounceCount": 1,
"suppressedAt": "2026-07-15T09:00:00.000Z",
"delistable": false
},
{
"email": "reporter@example.com",
"suppressionType": "complaint",
"reason": "complaint",
"scope": "company",
"source": "bounced_email",
"bounceType": null,
"bounceSubType": null,
"bounceCount": null,
"suppressedAt": "2026-06-02T11:30:00.000Z",
"delistable": false
}
],
"total": 2,
"page": 1,
"limit": 25,
"hasMore": false,
"sortBy": "suppressedAt",
"sortOrder": "desc"
}
```
```json 401 theme={null}
{ "success": false, "error": "Invalid API key" }
```
```json 403 theme={null}
{ "success": false, "error": "No company selected" }
```
# Remove Recipient Suppression
Source: https://docs.sequenzy.com/api-reference/suppressions/remove
DELETE /api/v1/suppressions/{email}
Remove a workspace soft-bounce escalation for one associated recipient.
Removes a `soft_bounce_escalation` for one exact recipient associated with the
selected company. If a company subscriber was in the `bounced` state, it is
reactivated and restored to its active list audiences.
Only the selected workspace's `company` row is cleared. Platform-wide
`invalid_recipient` and `unknown_hard_bounce` rows are protected, as are other
workspaces' scoped rows.
Global invalid-recipient, company hard-bounce, complaint, and unsubscribe
protections are never removed. Requests for a protected suppression return
`409`. Confirm that the temporary delivery problem is resolved before removing
a soft-bounce escalation.
## Request
Exact recipient email address. The address must already be associated with the
selected company through a subscriber or email-send record.
Accepted and ignored. It previously limited a provider-side suppression
lookup, which no longer happens.
```bash theme={null}
curl -X DELETE \
"https://api.sequenzy.com/api/v1/suppressions/user%40example.com" \
-H "Authorization: Bearer API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"email": "user@example.com",
"removed": true,
"removedLocalBounce": true,
"removedSesRegions": [],
"reactivatedSubscriberIds": ["subscriber_123"],
"remainingSuppression": {
"email": "user@example.com",
"knownRecipient": true,
"suppressed": false,
"local": {
"suppressed": false,
"suppressionType": null,
"reason": null,
"source": null,
"suppressionId": null,
"scope": null,
"delistable": false
},
"ses": {
"regionsChecked": [],
"entries": []
}
}
}
```
```json 404 theme={null}
{
"success": false,
"error": "Cannot remove suppression for user@example.com: the address is not associated with this company."
}
```
```json 409 theme={null}
{
"success": false,
"error": "Cannot remove suppression for user@example.com: this suppression is protected. Only company-scoped soft-bounce escalations can be removed."
}
```
The operation is idempotent: if no bounce suppression remains, it returns
`removed: false` with the current status.
# Get Sync Rules
Source: https://docs.sequenzy.com/api-reference/sync-rules/get
GET /api/v1/sync-rules
Get the company's effective sync rules
Returns the sync rules currently in effect: the automatic tag changes applied
when events fire. New companies start with an empty rule set. Legacy companies
may still inherit the optional SaaS/ecommerce platform preset; `isDefault`
reports whether that inherited preset is active.
## Request
No parameters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/sync-rules" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"syncRules": [
{
"triggerEvent": "ecommerce.order_placed",
"actions": {
"addTags": ["ecommerce.customer"],
"removeTags": ["ecommerce.in_cart", "ecommerce.in_checkout"]
}
}
],
"isDefault": true
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Update Sync Rules
Source: https://docs.sequenzy.com/api-reference/sync-rules/update
PUT /api/v1/sync-rules
Replace the company's sync rules
Replaces the full sync rule set, or opts into the legacy SaaS/ecommerce platform
preset when `null` is sent. This is not a partial update - fetch the current rules with
[Get Sync Rules](/api-reference/sync-rules/get), edit them, and send the
whole set back.
## Request
Full replacement rule set. Send `[]` to disable sync rules, or `null` to opt
into the inherited SaaS/ecommerce platform preset. Each rule has
`triggerEvent`, `actions.addTags`, `actions.removeTags`, and optional
`conditions` (`requiresTags`, `requiresNotTags`, and `purchasedProduct` with
`tags`, `collectionIds`, `productTypes`, or `vendors` for commerce events that
carry products).
Tag buyers of a product tag:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sync-rules" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"syncRules": [
{
"triggerEvent": "ecommerce.order_placed",
"actions": { "addTags": ["vinyl-collector"], "removeTags": [] },
"conditions": {
"purchasedProduct": { "tags": ["Vinyl"] }
}
}
]
}'
```
Opt into the platform preset:
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/sync-rules" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"syncRules": null}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Saved 1 sync rule.",
"syncRules": [
{
"triggerEvent": "ecommerce.order_placed",
"actions": { "addTags": ["vinyl-collector"], "removeTags": [] },
"conditions": {
"purchasedProduct": { "tags": ["Vinyl"] }
}
}
],
"isDefault": false
}
```
```json 400 theme={null}
{
"success": false,
"error": "syncRules.0.triggerEvent: String must contain at least 1 character(s)"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Create Tag
Source: https://docs.sequenzy.com/api-reference/tags/create
POST /api/v1/tags
Create a tag definition
Create a tag definition for your company. Tag names are normalized to lowercase with spaces replaced by hyphens (for example, `VIP Customer` becomes `vip-customer`).
## Request
Tag name. Normalized to lowercase with spaces replaced by hyphens.
Tag color. One of: `gray`, `red`, `orange`, `amber`, `yellow`, `lime`,
`green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`,
`purple`, `fuchsia`, `pink`, `rose`. Defaults to `gray`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "premium", "color": "blue"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"tag": {
"id": "tag_abc123",
"name": "premium",
"color": "blue",
"isSystem": false
}
}
```
```json 400 theme={null}
{
"error": "Invalid tag color"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 409 theme={null}
{
"error": "A tag with this name already exists"
}
```
```json 500 theme={null}
{
"error": "Failed to create tag"
}
```
# Delete Tag
Source: https://docs.sequenzy.com/api-reference/tags/delete
DELETE /api/v1/tags/{tagId}
Delete a tag definition
Delete a tag definition and remove the tag from all subscribers that have it. Tags that are referenced by sequence steps (add tag, remove tag, tag triggers, conditions, or branches) cannot be deleted until those steps are removed. System tags cannot be deleted.
## Request
Tag definition ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/tags/tag_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "Cannot delete system tags"
}
```
```json 404 theme={null}
{
"error": "Tag not found"
}
```
```json 409 theme={null}
{
"error": "Cannot delete tag: it's used in 2 sequence(s): Welcome Flow, Onboarding"
}
```
# List Tags
Source: https://docs.sequenzy.com/api-reference/tags/list
GET /api/v1/tags
List tag definitions
List tag definitions for the authenticated company.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/tags" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"tags": [
{
"id": "tag_abc123",
"name": "premium",
"color": "orange",
"isSystem": false
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Update Tag
Source: https://docs.sequenzy.com/api-reference/tags/update
PATCH /api/v1/tags/{tagId}
Update a tag definition's color
Update a tag definition's color. Tag names cannot be changed, and system tags cannot be updated.
## Request
Tag definition ID.
Tag color. One of: `gray`, `red`, `orange`, `amber`, `yellow`, `lime`,
`green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`,
`purple`, `fuchsia`, `pink`, `rose`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/tags/tag_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"color": "green"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"tag": {
"id": "tag_abc123",
"name": "premium",
"color": "green",
"isSystem": false
}
}
```
```json 400 theme={null}
{
"error": "Invalid tag color"
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 403 theme={null}
{
"error": "Cannot update system tags"
}
```
```json 404 theme={null}
{
"error": "Tag not found"
}
```
# Cancel Team Invitation
Source: https://docs.sequenzy.com/api-reference/team/cancel-invitation
DELETE /api/v1/team/invitations/{invitationId}
Cancel a pending team invitation
Cancel a pending or expired team invitation. The invitation link stops working immediately. Requires owner or admin access.
## Request
Invitation ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/team/invitations/inv_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "This invitation has already been accepted."
}
```
```json 400 theme={null}
{
"success": false,
"error": "This invitation has been cancelled. Create a new invitation instead."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "You need admin access to manage team members."
}
```
```json 404 theme={null}
{
"success": false,
"error": "Invitation not found"
}
```
# Invite Team Member
Source: https://docs.sequenzy.com/api-reference/team/invite
POST /api/v1/team/invitations
Invite a team member by email
Invite someone to your team. If the email belongs to an existing Sequenzy user, they are added to the team immediately and the response includes `member`. Otherwise an invitation email is sent and the response includes `invitation`. Invitations expire after 7 days.
Requires owner or admin access. Only the company owner can grant billing access.
Invitation emails are rate limited per company and per inviting user to prevent abuse. If you hit the limit, the endpoint returns `429` - wait before inviting more members or contact support for a higher limit.
## Request
Email address to invite.
Role for the new member: `admin`, `marketer`, `viewer`, or `restricted`.
Marketers create, edit, and send campaigns and sequences and manage
subscribers, lists, tags, and segments, but cannot access transactional
emails, settings, billing, API keys, integrations, or the team. Restricted
members can open direct campaign links only.
Transactional email restrictions also apply when a campaign or sequence references
a transactional template, including alternate email styles and A/B variants.
Marketers cannot read, edit, copy, render, send, or share that protected content,
including its sent-email history, through the dashboard, API, CLI, or MCP.
Whether the member can manage billing. Defaults to `false`. Only the company
owner can set this to `true`. Marketer and restricted members cannot manage
billing.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/team/invitations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "newhire@example.com", "role": "viewer"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"invitation": {
"id": "inv_abc123",
"email": "newhire@example.com",
"role": "viewer",
"canManageBilling": false,
"status": "pending",
"invitedAt": "2026-06-11T10:00:00.000Z",
"expiresAt": "2026-06-18T10:00:00.000Z"
}
}
```
```json 200 theme={null}
{
"success": true,
"member": {
"id": "member_abc123",
"kind": "member",
"userId": "user_abc123",
"role": "admin",
"canManageBilling": false,
"user": {
"id": "user_abc123",
"name": "John Marketer",
"email": "john@example.com",
"image": null
},
"status": "joined",
"createdAt": "2026-06-11T10:00:00.000Z",
"invitedAt": "2026-06-11T10:00:00.000Z",
"expiresAt": null,
"invitedBy": {
"id": "user_owner123",
"name": "Jane Founder",
"email": "jane@example.com"
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid email address"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Cannot add the company owner as a team member."
}
```
```json 403 theme={null}
{
"success": false,
"error": "You need admin access to manage team members."
}
```
```json 403 theme={null}
{
"success": false,
"error": "Only the company owner can grant billing access."
}
```
```json 400 theme={null}
{
"success": false,
"error": "Limited team members cannot manage billing."
}
```
```json 409 theme={null}
{
"success": false,
"error": "This user is already a member of your team."
}
```
```json 409 theme={null}
{
"success": false,
"error": "An invitation is already pending for this email."
}
```
```json 429 theme={null}
{
"success": false,
"error": "Too many team invitations were sent recently. Wait before inviting more members, or contact support if you need a higher limit."
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to send invitation email"
}
```
# List Team Members
Source: https://docs.sequenzy.com/api-reference/team/list
GET /api/v1/team
List team members and pending invitations
List everyone with access to the company: the owner, joined team members, and pending or expired invitations. Any team member can call this endpoint.
## Request
No parameters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/team" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"members": [
{
"id": "owner",
"kind": "owner",
"userId": "user_owner123",
"role": "owner",
"canManageBilling": true,
"user": {
"id": "user_owner123",
"name": "Jane Founder",
"email": "jane@example.com",
"image": null
},
"status": "joined",
"createdAt": null,
"invitedAt": null,
"expiresAt": null,
"invitedBy": null
},
{
"id": "member_abc123",
"kind": "member",
"userId": "user_abc123",
"role": "admin",
"canManageBilling": false,
"user": {
"id": "user_abc123",
"name": "John Marketer",
"email": "john@example.com",
"image": null
},
"status": "joined",
"createdAt": "2026-05-20T10:00:00.000Z",
"invitedAt": "2026-05-20T10:00:00.000Z",
"expiresAt": null,
"invitedBy": null
},
{
"id": "inv_abc123",
"kind": "invitation",
"userId": null,
"role": "restricted",
"canManageBilling": false,
"user": {
"id": null,
"name": null,
"email": "newhire@example.com",
"image": null
},
"status": "pending",
"createdAt": "2026-06-10T09:00:00.000Z",
"invitedAt": "2026-06-10T09:00:00.000Z",
"expiresAt": "2026-06-17T09:00:00.000Z",
"invitedBy": {
"id": "user_owner123",
"name": "Jane Founder",
"email": "jane@example.com"
}
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "You do not have access to this team."
}
```
```json 404 theme={null}
{
"success": false,
"error": "Company not found"
}
```
# Check Template
Source: https://docs.sequenzy.com/api-reference/templates/check
POST /api/v1/templates/{templateId}/check
Find broken links, placeholders and content problems in a template
Run the email check on a saved template: the same rules as the email checker in the editor, plus live verification of every link and image. Use it after editing a template, or before you wire it to a transactional email.
This endpoint is read-only. It requires the `templates:read` scope and never
sends or modifies anything. It uses `POST` only so personalization input can
travel in a request body.
## Request
Template ID. A transactional email's ID or slug also works; transactional
templates are checked without the unsubscribe-link rule, since they carry no
unsubscribe footer.
Check as this stored subscriber: picks their localization and reports merge
tags and conditions that would not resolve for them. Link and content rules
always run on the email as written. Use this or `subscriber`, not both.
Requires the `subscribers:read` scope as well.
Check as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Extra merge variables layered over the contact's attributes.
Check a specific localization instead of deriving it from the contact.
Verify every link and image over the network. Pass `false` for a fast,
rules-only check that makes no requests.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/tmpl_abc123/check" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
## What gets checked
Links and images are collected from every version recipients can receive: each A/B variant and each translation. Pass `variantId` or `locale` to check only that version.
**Links and images, live.** Every web link and image is requested from our servers (HEAD, then GET when a server refuses HEAD), following up to 8 redirects. Each one comes back with a `status`:
| Status | Meaning |
| - | - |
| `ok` | Answered with a success status, after any redirects. |
| `broken` | Page not found (404/410), the domain doesn't exist, an SSL certificate problem, a redirect loop, or a rejected request. Images that don't return an image also count. |
| `invalid` | Can never work for recipients: a rule already found it unusable (for example localhost, a private address, a mistyped `https://` or no `https://` at all), or the domain points to a private address. |
| `server_error` | The server answered with a 5xx error. Often temporary. |
| `unreachable` | No answer in time, or the connection was refused or reset. |
| `restricted` | The site refused an automated check (for example 401, 403 or 429), or redirects to itself to set a cookie. It may work fine in a browser, so it is never counted as broken. |
| `personalized` | Built from merge tags filled in per recipient, such as `{{event.orderUrl}}`. Only its syntax is checked. |
| `not_checked` | `mailto:` and `tel:` links, links not reached before the 25-second time limit, or `links` was `false`. |
`{{company.url}}` is resolved from your company website before checking. If no website is set, those links are reported as `links.missing-website`.
**Links, without a request.** Placeholder links (`#`, a bare `https://`), buttons with no link, script links, links missing `https://` or relative links, mistyped schemes such as `htps://`, unencoded spaces, incomplete domains, localhost and private addresses, tunnel and deploy-preview URLs, staging subdomains, placeholder domains such as `example.com`, `http://` links, link shorteners, raw IP addresses, link text that shows a different domain than the link goes to, invalid `mailto:`/`tel:` links, and unclosed merge tags in URLs.
**Images.** Broken or non-image URLs, images over 1 MB, embedded base64 images, local file paths, placeholder image services, and missing alt text.
**Content.** Leftover placeholder copy (lorem ipsum, `[First Name]`, TODO), name merge tags without a fallback, spam-trigger wording, excessive capitals, profanity, too many or no links, a missing unsubscribe link in marketing email, empty conditional branches, and broken merge tag syntax.
**Subject and preview text.** Length, placeholder text, a fake `Re:` or `Fwd:` prefix, spam-trigger wording, capitals, punctuation, emoji, and preview text that repeats the subject.
**Rendered HTML.** Gmail clipping risk, button contrast and tap-target size, and features specific email clients drop.
Sender settings (From and Reply-To) are not part of this check.
## Response
`true` when the check ran.
0 to 100 across subject (25%), preview text (15%) and content (40%), rescaled
to 100 because sender settings aren't part of this check. The editor also
weighs your sender (20%), so its score can differ slightly for the same email.
`A`, `B`, `C`, `D` or `F`.
Predicted inbox tab: `Primary`, `Promotions` or `Spam`.
`subject`, `preview` and `content`, each with `score` and `maxScore`.
Findings, errors first. Each has `category`, `severity` (`error` to fix before
sending, `warning` to review, `info` for tips) and `message`, plus a stable
`rule` ID such as `links.broken` or `content.placeholder-text`. When the
finding has one location, `blockId` names the block to edit and `url` the link
or image concerned.
Every link and image: `url`, `kind` (`link` or `image`), `label`, `blockId`,
`blockType`, `status`, `httpStatus`, `finalUrl` (after redirects), `message`,
and `findings` from the rules that need no request.
Counts: `total`, `checked`, `ok`, `broken` (broken and invalid), `warnings`
(server\_error and unreachable), `restricted` and `notChecked` (personalized
and not\_checked).
`false` when you passed `links: false`.
Subject line with merge tags resolved for the checked contact.
Preview text with merge tags resolved, or `null` when unset.
Localization the check used.
Merge tags that did not resolve, as on the [render
endpoint](/api-reference/templates/render).
Block conditions this check could not decide, as on the [render
endpoint](/api-reference/templates/render).
What was checked: `type`, `id` and `variantId`.
Results for the same URL are cached for up to 10 minutes (1 minute for failures), so repeating a check right after fixing a broken link can briefly return the old answer. Live checks are limited to 30 calls a minute per API key and company, and 60 a minute per API key across companies; past that you get `429`, and `links: false` still works.
```json 200 theme={null}
{
"success": true,
"score": 79,
"grade": "C",
"placement": "Promotions",
"categories": {
"subject": { "score": 100, "maxScore": 100 },
"preview": { "score": 80, "maxScore": 100 },
"content": { "score": 65, "maxScore": 100 }
},
"issues": [
{
"category": "content",
"severity": "error",
"rule": "links.broken",
"message": "Broken link: \"Shop the sale\" (acme.com/sale) - Page not found (404).",
"blockId": "block-hero",
"url": "https://acme.com/sale"
},
{
"category": "content",
"severity": "error",
"rule": "links.missing-scheme",
"message": "Missing https:// - email clients won't open this: \"new arrivals\" (www.acme.com/new).",
"blockId": "block-intro",
"url": "www.acme.com/new"
}
],
"links": [
{
"url": "https://acme.com/sale",
"kind": "link",
"label": "Shop the sale",
"blockId": "block-hero",
"blockType": "hero",
"status": "broken",
"httpStatus": 404,
"finalUrl": null,
"message": "Page not found (404)",
"findings": []
}
],
"linkSummary": {
"total": 6,
"checked": 4,
"ok": 3,
"broken": 1,
"warnings": 0,
"restricted": 0,
"notChecked": 2
},
"linksChecked": true,
"subject": "New arrivals picked for you",
"previewText": "Fresh styles and restocked favorites",
"locale": "en",
"unresolvedMergeTags": [],
"unevaluatedConditions": [],
"entity": { "type": "template", "id": "tmpl_abc123", "variantId": null }
}
```
```json 400 theme={null}
{
"error": "Provide either subscriberId or subscriber, not both."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 429 theme={null}
{
"error": "Too many link checks. Try again in a minute, or pass links: false for a rules-only check."
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Create Template
Source: https://docs.sequenzy.com/api-reference/templates/create
POST /api/v1/templates
Create an email template
Create a reusable email template with exactly one content source: `prompt` for Sequenzy-generated branded native blocks, `blocks` for finished caller-supplied content, or `html` for preserved/imported markup. `style` and `tone` are valid only with `prompt`; subject and preview values may override generated values.
Creating a standalone copy of a saved email or gallery design and AI rewriting
within that design are currently dashboard-only workflows. This endpoint has no source-template copy
operation, and `prompt` does not preserve an existing layout. See the
[documented interface exception](/concepts/email-templates#availability-across-interfaces)
for the scope and supported alternatives.
## Request
Template name. Sequenzy stores API-created templates with a `[Template]`
prefix.
Email subject line. Required with `html` or `blocks`; optional with `prompt`,
where it overrides the generated subject.
Optional inbox preview override. With `prompt`, the generated preview is used
when this field is omitted.
Natural-language request for Sequenzy to generate branded native template
blocks. Use this instead of `html` or `blocks`.
Optional generation style. Valid only with `prompt`. Pass `designed` or
`plain` to force the designed or plain-text email style; other values are
freeform prompt guidance. Defaults to your company's email style preference
(designed unless you chose plain text).
Optional generation tone. Valid only with `prompt`.
Raw HTML body. Use this or `blocks`, not both.
Sequenzy email blocks. Use this or `html`, not both. Put block styling under
`styles`; top-level style keys like `backgroundColor`, `backgroundOpacity`,
`textColor`, `textAlign`, `borderColor`, `borderWidth`, and `borderRadius` are
normalized into `styles`.
Label names to assign to the template. Missing labels are created
automatically. The API also accepts `label` as a compatibility alias.
Save as a reusable master design. Master designs are offered first when a
dashboard sequence step or campaign starts from an existing email, and
starting from one always creates an independent copy.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"prompt": "Welcome new customers and explain the next steps",
"tone": "friendly"
}'
```
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"subject": "Welcome!",
"labels": ["edm"],
"html": "
Thanks for joining.
"
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"template": {
"id": "email_abc123",
"name": "[Template] Welcome",
"subject": "Welcome!",
"labels": ["edm"],
"isTemplate": false
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide exactly one of prompt, html, or blocks."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create template"
}
```
# Create Template from Example
Source: https://docs.sequenzy.com/api-reference/templates/create-from-example
POST /api/v1/templates/from-example
Remix an email gallery example into a new template written for your brand
Remix a public email from the [Sequenzy email gallery](https://sequenzy.com/email-examples) into a new email template for your company. The new email keeps the example's exact layout and design. AI rewrites every piece of text for your company, and Sequenzy swaps in your logo, brand color and website links. The example brand is never named, and none of its copy, offers or claims are reused. Its legal footer is removed; your own compliant footer is added when you send.
The new email is saved as a reusable HTML template (it opens in the HTML editor), the same as **Remix this email** in the dashboard. Review it before you send it.
Generation usually takes 5 to 20 seconds. Nothing is saved when generation fails, so retrying is safe. Every successful call creates another template.
Requires the `templates:write` scope.
## Request
Identify the example with `url`, or with `brand` and `email`. Sending both returns 400.
Gallery email page URL, such as
`https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear`.
Emails in a sequence open on their sequence page, so their URL is the sequence
URL with the email in the hash, such as
`https://sequenzy.com/email-examples/brands/linear/sequences/onboarding#email-welcome-to-linear`;
that remixes the one email. A sequence URL without an email returns 400; use
[Create Sequence from Example](/api-reference/sequences/create-from-example)
for sequences.
Gallery brand slug, from `/email-examples/brands/{brand}/...`. Use with
`email`.
Gallery email slug, from `/emails/{email}`. Use with `brand`.
Optional direction for the new email, up to 1000 characters. It takes priority
over the example, for instance "Announce our mobile app".
Template name, up to 200 characters. Defaults to the generated subject
followed by "(remix of )".
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/from-example" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear",
"brief": "Mention our 14-day free trial"
}'
```
## Response
The new template: `id`, `name`, `subject`, `previewText` and `isTemplate`
(always `true`). Edit it with [Update
Template](/api-reference/templates/update).
The gallery example it was modeled on: `brand`, `subject` and `url`.
```json 200 theme={null}
{
"success": true,
"template": {
"id": "email_8f2kL9qW",
"name": "Welcome to Acme (remix of Linear)",
"subject": "Welcome to Acme",
"previewText": "Your first project takes two minutes",
"isTemplate": true
},
"example": {
"brand": "Linear",
"subject": "Welcome to Linear",
"url": "https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear"
},
"message": "Template created with the example's design, rewritten for your brand. Review it before sending."
}
```
```json 400 theme={null}
{
"error": "Provide either url or brand and email, not both."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "That gallery email was not found. It may have been removed.",
"code": "EXAMPLE_NOT_FOUND"
}
```
```json 500 theme={null}
{
"error": "AI did not generate any email content."
}
```
A 404 with code `EMAIL_GALLERY_UNAVAILABLE` means the email gallery is not available yet. A 503 with code `REMIX_FAILED` means the rewrite came back incomplete; nothing was saved, so retry. A 422 means a field failed validation, such as a `brief` over 1000 characters.
# Delete Template
Source: https://docs.sequenzy.com/api-reference/templates/delete
DELETE /api/v1/templates/{templateId}
Delete an unused email template
Delete an email template. Templates that are still used by a campaign, sequence, or transactional email cannot be deleted.
## Request
Template ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/templates/email_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Template deleted",
"templateId": "email_abc123"
}
```
```json 400 theme={null}
{
"success": false,
"error": "This email is used in a campaign. Remove it from there first."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Template not found"
}
```
# Get Template
Source: https://docs.sequenzy.com/api-reference/templates/get
GET /api/v1/templates/{templateId}
Get one email template
Get a saved email template. You can pass an email template ID; transactional email IDs and slugs are also resolved for compatibility.
Templates are saved email bodies, so this also resolves the `emailId` returned by [Get Campaign](/api-reference/campaigns/get) - useful for reading the `blocks` of an email designed in the dashboard.
## Request
Template ID, transactional email ID, or transactional slug.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/templates/email_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"template": {
"id": "email_abc123",
"companyId": "company_abc123",
"name": "[Template] Welcome",
"subject": "Welcome!",
"previewText": null,
"labels": ["edm"],
"isTemplate": true,
"blocks": [],
"emailPreset": "branded",
"fontFamily": null,
"shareUrl": null,
"createdAt": "2026-05-01T10:30:00Z",
"updatedAt": "2026-05-01T10:30:00Z",
"emailLocalizationConfig": {
"enabled": false,
"locales": []
},
"localizations": []
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Template not found"
}
```
`emailPreset` is the body's Style > Format: `branded` (company logo and full
footer), `minimal` (no logo, simple footer), or `null` when the entire email
is a single raw HTML block. Sequence steps, campaigns, and transactional
templates report the same field.
`isTemplate` is true for reusable master designs. In the dashboard these are
offered first when a sequence step or campaign starts from an existing email,
and starting from one always creates an independent copy. Set it with [Create
Template](/api-reference/templates/create) or [Update
Template](/api-reference/templates/update).
When the email belongs to an A/B test variant and the key has `ab_tests:read`,
the response includes `template.abTests[]`. Each entry has an `abTestId`,
every matching `variantId` / `variantLabel`, and a `contentEditing` pointer at
`update_ab_test_variant`. Campaign variants can share one email, so use the
complete variants list. Content changes must go through that path, not [Update
Template](/api-reference/templates/update).
# List Templates
Source: https://docs.sequenzy.com/api-reference/templates/list
GET /api/v1/templates
List email templates
List saved email templates for the authenticated company. You can filter by label.
Templates are the company's saved email bodies. That includes standalone templates created through this API, plus the bodies behind campaigns and transactional emails, so an email you design in the dashboard appears here too. A campaign's `emailId` (returned by [Get Campaign](/api-reference/campaigns/get)) points at its entry in this list, and any template ID can be passed as `templateId` to [Create Campaign](/api-reference/campaigns/create) to reuse the design. Bodies are kept when the campaign or transactional email that used them is deleted, so some entries may no longer have an owner. Content snapshots of code-managed transactional emails (created by [sends with `trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code)) are not listed: they hold one recipient's real content and are deleted with their email.
Results are newest first and paginated: 50 per page by default, up to 100. Because every campaign body counts toward the list, it grows with your sending history - page with `offset` while `pagination.hasMore` is true, or use a label filter to keep reusable designs separate from one-off campaign content.
## Request
Optional label name filter. Only templates assigned this label are returned.
Filter to reusable master designs (`true`) or everything else (`false`).
Master designs are what sequence steps and campaigns can start from; starting
from one always creates an independent copy. Omit to list every body.
Templates per page, 1-100. Values above 100 are clamped to 100.
Templates to skip before returning results.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/templates?label=edm" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
# Only reusable master designs
curl "https://api.sequenzy.com/api/v1/templates?isTemplate=true" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash theme={null}
# Second page of a long template history
curl "https://api.sequenzy.com/api/v1/templates?limit=100&offset=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"companyId": "company_abc123",
"emailLocalizationConfig": {
"enabled": false,
"locales": []
},
"templates": [
{
"id": "email_abc123",
"name": "[Template] Welcome",
"subject": "Welcome!",
"previewText": null,
"emailPreset": "branded",
"labels": ["edm"],
"isTemplate": true,
"createdAt": "2026-05-01T10:30:00Z",
"updatedAt": "2026-05-01T10:30:00Z",
"localizations": []
}
],
"pagination": {
"limit": 50,
"offset": 0,
"count": 50,
"total": 137,
"hasMore": true
}
}
```
```json 400 theme={null}
{
"error": "limit must be a positive integer up to 100"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
`emailPreset` is each body's Style > Format: `branded`, `minimal`, or `null`
for a single raw HTML block. Use it to check chrome consistency across bodies
without fetching each one.
# Preview Template from Example
Source: https://docs.sequenzy.com/api-reference/templates/preview-from-example
POST /api/v1/templates/from-example/preview
See an email gallery example written for your brand without saving it
Rewrite a public email from the [Sequenzy email gallery](https://sequenzy.com/email-examples) and get it back without saving anything. It is the same rewrite as [Create Template from Example](/api-reference/templates/create-from-example): the example's layout and design, every word written for your brand, and your logo, brand color and links swapped in.
By default the preview is written for your company's brand profile. Pass `website` to write it for another site instead, such as a prospect or a client. The site is looked up like the public gallery's **See it for your brand** preview.
Generation usually takes 5 to 20 seconds. Each call writes a new version. To keep one, call Create Template from Example; it writes the email again for your own brand.
Requires the `templates:write` scope.
## Request
Identify the example with `url`, or with `brand` and `email`. Sending both returns 400.
Gallery email page URL, such as
`https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear`,
or a sequence page URL ending in `#email-{email}` for an email in a sequence.
A sequence URL without an email returns 400; use [Preview Sequence from
Example](/api-reference/sequences/preview-from-example) for sequences.
Gallery brand slug, from `/email-examples/brands/{brand}/...`. Use with
`email`.
Gallery email slug, from `/emails/{email}`. Use with `brand`.
Optional direction, up to 1000 characters. It takes priority over the example.
Optional site or domain, such as `acme.com`, to write the preview for instead
of your own brand. Its registrable domain is used, so
`https://www.acme.com/pricing` becomes `acme.com`. A value that is not a site
returns 400.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/from-example/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand": "linear",
"email": "welcome-to-linear",
"website": "acme.com"
}'
```
## Response
The unsaved rewrite: `brandName` (whose brand it is written in), `website`
(the normalized `website`, or `null` for your own brand), `subject`,
`previewText` and `html`.
The gallery example it is modeled on: `brand`, `subject` and `url`.
```json 200 theme={null}
{
"success": true,
"preview": {
"brandName": "Acme",
"website": "acme.com",
"subject": "Welcome to Acme",
"previewText": "Your first project takes two minutes",
"html": "..."
},
"example": {
"brand": "Linear",
"subject": "Welcome to Linear",
"url": "https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear"
},
"message": "Preview only: nothing was saved. Call POST /templates/from-example to save a template."
}
```
```json 400 theme={null}
{
"error": "Pass website as a site or domain, like acme.com."
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "That gallery email was not found. It may have been removed.",
"code": "EXAMPLE_NOT_FOUND"
}
```
A 404 with code `EMAIL_GALLERY_UNAVAILABLE` means the email gallery is not available yet. A 503 with code `REMIX_FAILED` means the rewrite came back incomplete; retry. A 422 means a field failed validation, such as a `brief` over 1000 characters.
# Render Template HTML
Source: https://docs.sequenzy.com/api-reference/templates/render
POST /api/v1/templates/{templateId}/render
Render a template to email-ready HTML for an embedded preview
Render a template to the exact email-safe HTML that would be sent. Use this to show a visual preview inside your own dashboard or email builder without duplicating Sequenzy's rendering logic.
The render applies everything the send path applies: your email theme and font, company branding and footer, localization, block visibility conditions, product recommendation blocks, and merge tags.
This endpoint is read-only. It requires the `templates:read` scope and never
sends or modifies anything. It uses `POST` only so personalization input can
travel in a request body.
## Request
Template ID. A transactional email's ID or slug also works, and renders its
underlying template without the unsubscribe footer.
Personalize as this stored subscriber. Use this or `subscriber`, not both.
Because the rendered HTML then carries that subscriber's details, this field
also requires the `subscribers:read` scope.
Personalize as an ad-hoc contact that does not need to exist in your account.
Accepts `email` (required), `firstName`, `lastName`, `customAttributes`, and
`tags`. Use this or `subscriberId`, not both.
Tags this ad-hoc contact carries, used to evaluate `tag` block conditions.
Nothing is stored. Without it a tag condition has no tags to read, so only the
else branch of a tag split can be previewed.
Extra merge variables layered over the contact's attributes.
Force a localization locale instead of deriving it from the contact.
Apply your company's auto-UTM link decoration, as a real send would. Defaults
to `false` so the preview shows your own clean URLs.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/tmpl_abc123/render" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subscriber": { "email": "anna@example.com", "firstName": "Anna" },
"variables": { "plan": "Pro" }
}'
```
## Checking merge tags
An email never shows a raw `{{TAG}}` to a recipient, so a tag that does not exist and a tag that is simply blank for this contact both render as an empty string. `unresolvedMergeTags` tells them apart:
* `"reason": "unknown"` - nothing provides that name, so it will be empty for every recipient. Usually a typo, or a tag carried over from another platform.
* `"reason": "no_value"` - the name is recognized but has no value for this contact.
A tag with a fallback is reported too when its name is unrecognized. `{{ subscriber.frstName | default: "there" }}` renders `there` for everybody, including the recipients whose first name is stored, and the HTML looks correctly personalized - so an empty `unresolvedMergeTags` array, not the rendered greeting, is what tells you personalization works. A recognized name that is merely blank for this contact is not reported when it has a fallback, since that is exactly what a fallback is for.
A name is only called `unknown` when the render had a source to check it against. Without the contact's attributes nothing is checkable at all - a bare `{{plan}}` reads the same attribute map as `{{subscriber.plan}}` - so pass a stored `subscriberId`, or an inline `subscriber` that includes `customAttributes`. Beyond that, `{{event.*}}` needs sample event properties in `variables`, since a real send fills those from the enrolling event, and `{{recommendedProducts.*}}` needs a stored `subscriberId` the catalog has something to recommend for. Without those, the tags come back as `no_value` rather than being reported as typos. An optional attribute that this contact happens not to have set is kept out of `unknown` by checking the names other contacts in the account carry, which is subscriber data: that check needs the `subscribers:read` scope, so a key without it may report such a name as `unknown`. `{{discount.*}}` is never checkable here: a template has no sequence position, so it is always reported as `no_value`.
Rendering a transactional email is a special case. Its tags are filled from the `variables` you pass on each [send](/api-reference/transactional/send), and those names carry no prefix marking them, so `{{RESET_URL}}` in a password-reset template is indistinguishable from a misspelling. Unless you pass `variables` here too, nothing in a transactional render is called `unknown`.
An empty array means every tag in the email resolved.
## Checking conditions
Conditional blocks split on subscriber data, and the fields that read stored subscriber state - `tag`, `segment`, `list`, `status`, `event`, purchases, and engagement - are evaluated per recipient at send time. A render can evaluate them for a stored `subscriberId`, and it can evaluate a `tag` condition for an inline contact that lists its own `tags`. Anything else it cannot check.
A condition it cannot check renders as `false`, the same fail-closed rule a live send uses, so the else branch appears in the HTML exactly as it would for a contact who genuinely does not match. `unevaluatedConditions` is what tells those apart:
* `"reason": "requires_stored_subscriber"` - the field reads stored subscriber state. Pass `subscriberId`, or for a `tag` condition pass `subscriber.tags`.
* `"reason": "invalid_filter"` - the stored condition is malformed, so a real send fails it closed too. `hint` carries the validation error.
* `"reason": "evaluation_failed"` - the lookup itself failed. The render is worth retrying.
An empty array means every condition in the email was actually evaluated, so the branches in the HTML are the branches this contact would receive.
## What is not in the HTML
Per-send click-redirect wrapping, the open tracking pixel, and unsubscribe click tracking are applied at send time against a real email send record, so they are never present in the returned HTML. The `tracking` flag controls auto-UTM decoration only.
## Responses
```json 200 theme={null}
{
"success": true,
"html": "...",
"subject": "Welcome aboard, Anna",
"previewText": "Here is what happens next",
"locale": "en",
"personalized": true,
"trackingApplied": false,
"unresolvedMergeTags": [{ "tag": "discountCode", "reason": "unknown" }],
"unevaluatedConditions": [
{
"field": "tag",
"operator": "contains",
"value": "extended",
"description": "Tag contains extended",
"reason": "requires_stored_subscriber",
"hint": "Pass subscriberId, or pass tags on the inline subscriber, to evaluate this condition."
}
],
"entity": {
"type": "template",
"id": "tmpl_abc123",
"variantId": null
}
}
```
```json 400 theme={null}
{
"error": "Provide either subscriberId or subscriber, not both."
}
```
```json 401 theme={null}
{
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: subscribers:read"
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Set Template Localization
Source: https://docs.sequenzy.com/api-reference/templates/set-localization
PUT /api/v1/templates/{templateId}/localizations/{locale}
Create or replace a caller-supplied localized template variant
Create or replace one localized variant with your own translated content. The
locale must be enabled in the company's localization settings and cannot be the
primary locale.
By default, the stored translation behaves as before: when automatic
translation on save is on, the next change to the original template replaces
it with a fresh AI translation. Pass `keepEdits: true` to protect it like an
edit made in the dashboard. It is then marked as edited (`editedAt`), automatic
translation on save keeps it, and its status becomes `stale` when the original
changes so you can review it. An explicit
[sync](/api-reference/templates/sync-localizations) still retranslates it
unless you pass `skipEdited: true`.
## Request
Template ID, transactional email ID, or transactional slug.
Enabled non-primary locale code, such as `es`, `fr`, or `pt-BR`.
Localized email subject line.
Optional localized inbox preview text.
Localized raw HTML. Provide exactly one of `html` or `blocks`.
Localized Sequenzy email blocks. Provide exactly one of `blocks` or `html`.
Protect this translation from automatic translation on save. Without it, any
earlier edit flag on this locale is cleared.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/templates/email_abc123/localizations/es" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Bienvenido",
"previewText": "Nos alegra que estés aquí",
"html": "
Gracias por registrarte.
"
}'
```
## Response fields
When this translation was last edited in the dashboard or stored with
`keepEdits: true`. `null` means automatic translation on save may replace it.
## Responses
```json 200 theme={null}
{
"success": true,
"templateId": "email_abc123",
"localization": {
"locale": "es",
"status": "synced",
"subject": "Bienvenido",
"previewText": "Nos alegra que estés aquí",
"blocks": [
{
"id": "generated-block-id",
"type": "text",
"content": "
Gracias por registrarte.
",
"variant": "html"
}
],
"sourceHash": "f4c3...",
"lastError": null,
"syncedAt": "2026-07-14T20:00:00Z",
"editedAt": "2026-07-14T20:00:00Z",
"createdAt": "2026-07-14T20:00:00Z",
"updatedAt": "2026-07-14T20:00:00Z"
}
}
```
```json 400 theme={null}
{
"error": "Locale \"es\" is not enabled for this company."
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Create Template Share Link
Source: https://docs.sequenzy.com/api-reference/templates/share-link-create
POST /api/v1/templates/{templateId}/share-link
Create or fetch the email's public view-in-browser link
Create a public, shareable "view in browser" link for an individual email - a transactional email, a sequence email, or a standalone template. The hosted page renders an anonymized copy - a sample contact instead of subscriber data, an inert unsubscribe link, and no open or click tracking - so you can forward the URL to anyone. Emails wired to a transactional template render without the unsubscribe footer, matching how they are sent.
The call is idempotent: if a link is already active, the same URL is returned with `created: false` instead of rotating it, so copies you have already shared keep working. Revoke it with [Revoke Share Link](/api-reference/templates/share-link-revoke). The current link is also reported as `shareUrl` on [Get Template](/api-reference/templates/get) (`null` when none is active).
Campaigns have their own campaign-level link that follows the A/B winning variant - use [Create Share Link](/api-reference/campaigns/share-link-create) on the campaign instead.
## Request
Email/template ID, or a transactional email's ID or slug. For a sequence
email, use the step's `emailId`.
This endpoint does not accept a request body.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/order-confirmation/share-link" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"shareUrl": "https://sequenzy.com/view/email/8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s",
"shareToken": "8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s",
"created": true
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Revoke Template Share Link
Source: https://docs.sequenzy.com/api-reference/templates/share-link-revoke
DELETE /api/v1/templates/{templateId}/share-link
Revoke the email's public view-in-browser link
Revoke the email's public share link. The hosted URL returns 404 immediately. Creating a new link later mints a different URL - previously distributed copies stay dead.
Returns `revoked: false` when the email had no active link, which is not an error.
## Request
Email/template ID, or a transactional email's ID or slug.
This endpoint does not accept a request body.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/templates/order-confirmation/share-link" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"revoked": true
}
```
```json 401 theme={null}
{
"error": "Invalid API key"
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Sync Template Localizations
Source: https://docs.sequenzy.com/api-reference/templates/sync-localizations
POST /api/v1/templates/{templateId}/localizations/sync
Queue AI translation for selected or all enabled template locales
Queue AI translation for selected enabled locales. Send an empty object to sync
every enabled non-primary locale. This explicit operation works even when the
company's automatic on-save localization mode is disabled.
Translation runs in the background. Each queued locale shows `syncing` until
the job writes `synced` (or `failed` with `lastError`); poll
[Get Template](/api-reference/templates/get) to follow it.
## Request
Template ID, transactional email ID, or transactional slug.
Enabled non-primary locale codes to sync. Omit to sync every enabled
non-primary locale.
Keep translations edited in the dashboard or stored with `keepEdits` (see
`editedAt` on [Set Template
Localization](/api-reference/templates/set-localization)) instead of
retranslating them. Kept locales are returned in `skippedLocales`. When every
requested locale is kept, the response is `200` with an empty `queuedLocales`.
Without this field, edited translations are retranslated and lose their edits,
as before.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/templates/email_abc123/localizations/sync" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"locales":["es","fr"]}'
```
## Response fields
Locales queued for AI translation.
Number of queued locales.
Requested locales kept because someone edited them. Only returned when
`skipEdited` is `true`.
## Responses
```json 200 theme={null}
{
"success": true,
"templateId": "email_abc123",
"queuedLocales": ["es", "fr"],
"queuedVariantCount": 2
}
```
```json 400 theme={null}
{
"error": "No supported non-primary locales are configured for this company."
}
```
```json 404 theme={null}
{
"error": "Template not found"
}
```
# Update Template
Source: https://docs.sequenzy.com/api-reference/templates/update
PUT /api/v1/templates/{templateId}
Update an email template
Update template metadata, labels, or content. You can pass an email template ID; transactional email IDs and slugs are also resolved for compatibility.
## Request
Template ID, transactional email ID, or transactional slug.
Updated template name.
Updated email subject line.
Updated inbox preview text. Send `null` to clear it.
Replacement HTML body. Use this or `blocks`, not both.
Replacement Sequenzy email blocks. Use this or `html`, not both. Put block
styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
Replacement label names. Send an empty array to clear labels. Missing labels
are created automatically. The API also accepts `label` as a compatibility
alias.
Mark (`true`) or unmark (`false`) this email as a reusable master design that
sequence steps and campaigns can start from.
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/templates/email_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Welcome to Acme", "previewText": "Everything you need to get started", "labels": ["edm"]}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"template": {
"id": "email_abc123",
"name": "[Template] Welcome",
"isTemplate": true,
"subject": "Welcome to Acme",
"previewText": "Everything you need to get started",
"labels": ["edm"]
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at least one of: name, subject, previewText, html, blocks, labels."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Template not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to update template"
}
```
Content on an A/B test variant cannot be changed here. The 400 names
`update_ab_test_variant` so test status and live-change confirmation stay
enforced. When the key also has `ab_tests:read`, the error includes
`abTests[]` with every matching test and variant; otherwise those protected
identifiers are omitted. [Get Template](/api-reference/templates/get) reports
the same collection on `template.abTests` before you write.
# Get Tracking Domain
Source: https://docs.sequenzy.com/api-reference/tracking-domain/get
GET /api/v1/tracking-domain
Get the tracking domain every sending domain uses
Get your company tracking domain: the one hostname every sending domain uses
for tracked links and opens, such as `links.example.com`. It never blocks
sending. While it is missing, unverified, broken or removed, links use
Sequenzy's shared tracking domain.
Requires the `account:read` scope.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/tracking-domain" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
The company tracking domain, or `null` when links use the shared Sequenzy
tracking domain.
The tracking hostname.
`not_started`, `pending`, `verified` or `failed`. It covers both the CNAME and
the HTTPS certificate. `not_started` means the DNS setup was not confirmed or
checked yet.
`true` while verified. New emails use this domain only while it is active.
`true` once this hostname has verified. A `failed` domain that verified before
is broken; one that never verified is an unfinished setup.
HTTPS certificate status reported by the tracking provider.
When it last became verified.
When it was last checked.
The latest verification problem, if any.
The record to publish: `type` (`CNAME`), `name` and `value`. Turn off proxying
for it.
```json 200 theme={null}
{
"success": true,
"trackingDomain": {
"domain": "links.example.com",
"status": "verified",
"active": true,
"everVerified": true,
"sslStatus": "active",
"verifiedAt": "2026-09-28T11:00:00Z",
"lastCheckedAt": "2026-09-28T12:00:00Z",
"error": null,
"cnameRecord": {
"type": "CNAME",
"name": "links.example.com",
"value": "links1.sequenzydns.com"
}
}
}
```
```json 200 (none set) theme={null}
{
"success": true,
"trackingDomain": null
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
# Remove Tracking Domain
Source: https://docs.sequenzy.com/api-reference/tracking-domain/remove
DELETE /api/v1/tracking-domain
Remove the company tracking domain
Remove your company tracking domain. New emails use Sequenzy's shared tracking
domain.
Links in emails you already sent through the removed domain stop working. To
move to another tracking domain without breaking them, use [Set Tracking
Domain](./set) instead and keep the old CNAME record.
Removing when no tracking domain is set succeeds with `removed: false`. The next
sending domain you add creates a new tracking domain on its root, as the first
one did.
Requires the `companies:manage` scope and owner or admin access.
## Request
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/tracking-domain" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`true` when a tracking domain was removed, `false` when none was set.
Always `null`.
```json 200 theme={null}
{
"success": true,
"removed": true,
"trackingDomain": null
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
```json 503 theme={null}
{
"error": "Cloudflare API request failed."
}
```
A `503` means the tracking provider could not release the hostname. The
tracking domain was kept, so you can retry.
# Set Tracking Domain
Source: https://docs.sequenzy.com/api-reference/tracking-domain/set
PUT /api/v1/tracking-domain
Set or change the tracking domain every sending domain uses
Set or change your company tracking domain. Every sending domain uses it for
tracked links and opens, so you set it up once.
* Any subdomain you control works, for example `links.example.com`. It does not
have to share a root with your sending domains. A bare domain such as
`example.com` is rejected because most DNS providers cannot hold a CNAME
there.
* After saving, publish the returned `trackingDomain.cnameRecord`. Sending never
waits for it: new emails use Sequenzy's shared tracking domain until it
verifies. Check it right away with [Verify Tracking Domain](./verify).
* Saving the current hostname again only rechecks it.
* Changing to a new hostname restarts verification for the new one. The
previous hostname keeps serving links in emails you already sent, as long as
you keep its old CNAME record.
Requires the `companies:manage` scope and owner or admin access.
## Request
```bash theme={null}
curl -X PUT "https://api.sequenzy.com/api/v1/tracking-domain" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "links.example.com" }'
```
## Body
The tracking hostname, a subdomain such as `links.example.com`. A leading
`https://` and a trailing path are ignored.
## Response fields
The next step, usually the CNAME record to add.
The saved tracking domain. See [Get Tracking Domain](./get) for its fields.
```json 200 theme={null}
{
"success": true,
"message": "Add a CNAME record for links.example.com pointing to links1.sequenzydns.com. Links use the shared tracking host until it verifies.",
"trackingDomain": {
"domain": "links.example.com",
"status": "pending",
"active": false,
"everVerified": false,
"sslStatus": "pending_validation",
"verifiedAt": null,
"lastCheckedAt": "2026-09-28T12:00:00Z",
"error": null,
"cnameRecord": {
"type": "CNAME",
"name": "links.example.com",
"value": "links1.sequenzydns.com"
}
}
}
```
```json 400 theme={null}
{
"error": "Use a subdomain for tracking, for example links.acme.com."
}
```
```json 403 theme={null}
{
"error": "You have view-only access to this company. Contact the owner to request edit permissions."
}
```
```json 409 theme={null}
{
"error": "This tracking domain is already connected."
}
```
```json 503 theme={null}
{
"error": "Cloudflare for SaaS is not configured."
}
```
A `503` means the tracking provider could not register the hostname. Nothing
was saved, so you can retry the same request.
# Verify Tracking Domain
Source: https://docs.sequenzy.com/api-reference/tracking-domain/verify
POST /api/v1/tracking-domain/verify
Check the tracking domain's DNS and HTTPS now
Check your company tracking domain's CNAME record and HTTPS certificate now,
instead of waiting for the automatic checks. Sequenzy keeps checking on its
own, and a short DNS outage never changes a verified domain: it has to fail
for at least 30 minutes before links move to the shared tracking domain.
Requires the `companies:manage` scope and owner or admin access.
## Request
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/tracking-domain/verify" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Whether the CNAME and the HTTPS certificate work.
The result or the next step, for example that the certificate is still being
issued.
The tracking domain after the check. See [Get Tracking Domain](./get) for its
fields.
```json 200 theme={null}
{
"success": true,
"verified": false,
"message": "DNS is correct. Cloudflare is still issuing the SSL certificate.",
"trackingDomain": {
"domain": "links.example.com",
"status": "pending",
"active": false,
"everVerified": false,
"sslStatus": "pending_validation",
"verifiedAt": null,
"lastCheckedAt": "2026-09-28T12:05:00Z",
"error": null,
"cnameRecord": {
"type": "CNAME",
"name": "links.example.com",
"value": "links1.sequenzydns.com"
}
}
}
```
```json 404 theme={null}
{
"error": "Add a tracking domain first."
}
```
```json 503 theme={null}
{
"error": "Cloudflare API request failed."
}
```
# Get Tracking Settings
Source: https://docs.sequenzy.com/api-reference/tracking-settings/get
GET /api/v1/tracking-settings
Get the company's email tracking configuration
Get how a company measures email engagement - account-wide and Transactional API open/click defaults, unsubscribe tracking, the attribution window, automatic UTM tagging, the company tracking domain, and inbound reply tracking - plus whether new contacts must confirm their subscription.
Use this to audit measurement readiness, to explain why opens or clicks are not being recorded, or to check how contacts enter the list before investigating bot or alias signups.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/tracking-settings" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Whether open tracking pixels are injected.
Whether links are rewritten for click tracking.
Whether Transactional API sends include an open-tracking pixel by default. The
account-wide `openTrackingEnabled` flag remains the master switch.
Whether Transactional API sends rewrite links for click tracking by default.
The account-wide `clickTrackingEnabled` flag remains the master switch.
Opt-in aggressive bot detection at tracking ingest: strict user-agent
patterns, datacenter and Microsoft Azure IPs, fast campaign and sequence
clicks from IPs clicking other recipients' emails, and cross-send IP sweeps.
Off by default. Enabling it filters more security-scanner traffic but can also
discard some real engagement, lowering reported open and click rates.
Whether to track unsubscribe link clicks. When false, Sequenzy unsubscribe
links go directly to [https://sequenzy.com](https://sequenzy.com), even with a custom tracking domain.
Actual unsubscribes and their email attribution are still recorded.
Hours after a send during which conversions are attributed to it.
Whether new contacts must confirm by email before they become subscribed. When
enabled, contacts added by forms, the API, and integrations start `pending`
until they click the confirmation email.
Confirmation email sent to pending contacts. `null` when double opt-in has
never been enabled; enabling it provisions one automatically.
Where the hosted confirmation page sends subscribers after they confirm.
`null` keeps them on the confirmation page, which is branded with the
company's name, logo, and colors.
Whether UTM parameters are appended to outbound links automatically.
Company tracking domain with its verification and SSL status. Every sending
domain uses it for tracked links and opens. `null` means links use the shared
Sequenzy tracking domain, as they also do while it is not verified. Manage it
with [Set Tracking Domain](../tracking-domain/set).
Inbound reply configuration: `inboundEmailEnabled`, `inboundReplyDomainMode`,
and `forwardReplies`.
## Responses
```json 200 theme={null}
{
"success": true,
"tracking": {
"openTrackingEnabled": true,
"clickTrackingEnabled": true,
"transactionalOpenTrackingEnabled": false,
"transactionalClickTrackingEnabled": false,
"strictBotFilteringEnabled": false,
"unsubscribeTrackingEnabled": true,
"defaultAttributionWindowHours": 24
},
"consent": {
"doubleOptInEnabled": true,
"doubleOptInEmailId": "email_abc123",
"doubleOptInRedirectUrl": "https://example.com/welcome"
},
"autoUtm": {
"enabled": true,
"settings": { "source": "sequenzy", "medium": "email" }
},
"trackingDomain": {
"domain": "links.acme.com",
"status": "verified",
"sslStatus": "active",
"verifiedAt": "2026-02-02T11:00:00Z",
"lastCheckedAt": "2026-05-01T06:00:00Z",
"error": null
},
"replyTracking": {
"inboundEmailEnabled": true,
"inboundReplyDomainMode": "sequenzy",
"forwardReplies": true
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: account:read"
}
```
# Update Tracking Settings
Source: https://docs.sequenzy.com/api-reference/tracking-settings/update
PATCH /api/v1/tracking-settings
Update the company's email tracking defaults
Update how a company measures email engagement - account-wide and Transactional API open/click defaults, unsubscribe tracking, the default attribution window, and automatic UTM tagging - plus whether new contacts must confirm their subscription before they become active.
Changes apply to emails sent afterwards. Account-wide open/click settings are
the master policy for every email; Transactional API defaults can narrow that
policy for API sends without changing campaigns or sequences. Per-send
`trackingSettings` can opt out again. Already-sent emails keep the links and
pixels they were rendered with.
Provide at least one field. Reply tracking (`replyTrackingEnabled`, `replyTrackingDomainMode`, `forwardReplies`) is updated through [Update Company](/api-reference/companies/update), and the company tracking domain is managed with [Set Tracking Domain](/api-reference/tracking-domain/set).
Requires the `companies:manage` scope.
## Request
Whether to embed the open-tracking pixel. Turning this off also stops open
rates from being recorded.
Whether to rewrite links through the click-tracking redirect. Turning this off
stops click rates and click-based automation triggers.
Whether Transactional API sends include the open-tracking pixel by default.
This cannot enable tracking when `openTrackingEnabled` is `false`.
Whether Transactional API sends rewrite links for click tracking by default.
This cannot enable tracking when `clickTrackingEnabled` is `false`.
Opt-in aggressive bot detection: strict user-agent patterns, datacenter and
Microsoft Azure IPs, fast campaign and sequence clicks from IPs clicking other
recipients' emails, and cross-send IP sweeps. Off by default. Enabling it
filters more security-scanner traffic but can also discard some real
engagement, lowering reported open and click rates.
Whether to track unsubscribe link clicks. When false, Sequenzy unsubscribe
links go directly to [https://sequenzy.com](https://sequenzy.com), even with a custom tracking domain.
Actual unsubscribes and their email attribution are still recorded. Omit this
field to keep the current setting; `null` is not accepted. Changes apply to
subsequent sends; already-delivered links are unchanged.
Default revenue attribution window in hours, between 1 and 720.
Whether new contacts must confirm by email before they become subscribed.
When enabled, contacts added by forms, the API, and integrations start
`pending` and are sent a confirmation email; they only become `active` after
clicking it, and they are never sent marketing email while pending. This is
the account-wide default that the per-request `optInMode` on
[Create Subscriber](/api-reference/subscribers/create) overrides.
Enabling it requires a sender profile, and provisions the confirmation email
automatically when the account does not have one yet. It does not change
contacts that are already active.
Where the hosted confirmation page sends subscribers after they confirm -
typically a thank-you or welcome page on your own site. Must be an `http(s)`
URL of at most 500 characters; a bare domain is normalized to `https://`. Pass
`null` (or an empty string) to clear it, keeping subscribers on the
confirmation page, which is branded with your company name, logo, and colors.
Whether UTM parameters are appended to outbound links automatically. Enabling
this when no parameters are stored seeds the platform defaults.
UTM templates - `source`, `medium`, `campaign`, `content`, `term` - merged
over the stored ones. Values support placeholders such as
`{{email.subject}}` and `{{link.text}}`. Set a field to `null` to stop
emitting that parameter, or send `null` for the whole object to reset every
parameter to the platform defaults.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/tracking-settings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"transactionalOpenTrackingEnabled": false, "transactionalClickTrackingEnabled": false}'
```
## Responses
The full tracking configuration is returned after the update, in the same shape as [Get Tracking Settings](/api-reference/tracking-settings/get).
```json 200 theme={null}
{
"success": true,
"message": "Updated tracking settings: transactionalOpenTrackingEnabled, transactionalClickTrackingEnabled.",
"tracking": {
"openTrackingEnabled": true,
"clickTrackingEnabled": true,
"transactionalOpenTrackingEnabled": false,
"transactionalClickTrackingEnabled": false,
"strictBotFilteringEnabled": false,
"unsubscribeTrackingEnabled": true,
"defaultAttributionWindowHours": 24
},
"consent": {
"doubleOptInEnabled": false,
"doubleOptInEmailId": null,
"doubleOptInRedirectUrl": null
},
"autoUtm": {
"enabled": false,
"settings": {}
},
"trackingDomain": null,
"replyTracking": {
"inboundEmailEnabled": true,
"inboundReplyDomainMode": "sequenzy",
"forwardReplies": true
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "defaultAttributionWindowHours must be a whole number between 1 and 720."
}
```
```json 400 theme={null}
{
"success": false,
"error": "A default sender profile is required before enabling double opt-in. Add a sending domain and sender profile first, otherwise the confirmation email can never be delivered and every new signup stays pending."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: companies:manage"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Company not found"
}
```
# Create Transactional Email
Source: https://docs.sequenzy.com/api-reference/transactional/create
POST /api/v1/transactional
Create a saved transactional email template
Create a transactional email template with exactly one content source: `prompt` for Sequenzy-generated transactional blocks, `blocks` for finished caller-supplied content, or `html` for preserved/imported markup. Prompt-created templates default to disabled; explicit HTML or blocks retain the compatibility default of enabled. Generated transactional footers omit unsubscribe treatment.
## Request
Company label names, trimmed and deduplicated. Missing names are created in
your company. Each name must be non-empty and at most 255 characters. Send
`[]` to clear assignments. Omit to start without labels. `null`, blank names,
and non-string entries are rejected.
Transactional email name.
API slug used when sending by slug. If omitted, Sequenzy generates one from
the name.
Email subject line. Required with `html` or `blocks`; optional with `prompt`,
where it overrides the generated subject.
Optional preview override. With `prompt`, the generated preview is used when
this field is omitted. Pass `null` to suppress preview text.
Natural-language request for Sequenzy to generate branded transactional blocks
with a footer that omits unsubscribe treatment.
Optional generation style. Valid only with `prompt`. Pass `designed` or
`plain` to force the designed or plain-text email style; other values are
freeform prompt guidance. Defaults to your company's email style preference
(designed unless you chose plain text).
Optional generation tone. Valid only with `prompt`.
Raw HTML body. Provide either `html` or `blocks`, not both.
Sequenzy email blocks. Provide either `blocks` or `html`, not both. Put block
styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
Whether this transactional email can be sent immediately. Defaults to `false`
with `prompt` and `true` with explicit `html` or `blocks`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Password Reset",
"slug": "password-reset",
"prompt": "Write a concise password reset email using {{RESET_URL}}"
}'
```
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Password Reset",
"slug": "password-reset",
"subject": "Reset your password, {{NAME}}",
"previewText": "Use this link to reset your password.",
"html": "
Reset link: {{RESET_URL}}
"
}'
```
## Responses
```json 201 theme={null}
{
"success": true,
"transactional": {
"id": "txn_abc123",
"name": "Password Reset",
"slug": "password-reset",
"emailId": "email_abc123",
"labels": ["Billing"],
"enabled": true,
"subject": "Reset your password, {{NAME}}",
"previewText": "Use this link to reset your password.",
"blocks": [
{
"id": "block_123",
"type": "text",
"content": "
Reset link: {{RESET_URL}}
",
"variant": "html"
}
],
"variables": ["NAME", "RESET_URL"],
"createdAt": "2026-05-01T10:30:00Z",
"updatedAt": "2026-05-01T10:30:00Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide exactly one of prompt, html, or blocks."
}
```
```json 400 theme={null}
{
"success": false,
"error": "No sender profile configured. Please create a sender profile first."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"error": "A transactional email with slug \"password-reset\" already exists"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
Label names appear in the response's `transactional.labels` array. Invalid
label names return `400`; invalid label types return `422`. Retrying the same
label replacement does not duplicate company labels or send an email. After
an uncertain update response, read the template before retrying. Label-only
updates preserve email content and translations.
Insufficient API-key permissions return `403`. Label support uses the existing transactional write permission; no additional label scope is required.
# Delete Transactional Email
Source: https://docs.sequenzy.com/api-reference/transactional/delete
DELETE /api/v1/transactional/{idOrSlug}
Permanently delete a saved transactional email template
Permanently delete a transactional email template by ID or slug. The slug stops sending and becomes free to reuse.
Already-sent deliveries are not affected: send history, stats, and stored HTML live on the deliveries themselves, so past sends stay visible in analytics and `GET /api/v1/email-sends` after the template is gone.
The email content is kept as a reusable template, the same way deleting a campaign leaves its email behind. The response returns it as `emailId`; pass that to `DELETE /api/v1/templates/{templateId}` to remove the content too.
Code-managed emails (`managedBy: "code"`, created by [sends with `trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code)) are the exception: their content is a snapshot of a real send, so it is deleted with the email, `deleted.emailDeleted` is `true`, and the returned `emailId` no longer exists. The next send with that `trackAs` creates a new code-managed email.
To stop sends without losing the template, [update](/api-reference/transactional/update) it with `enabled` set to `false` instead.
This endpoint requires an API key with the `transactional:delete` scope.
## Path Parameters
Transactional email ID or slug
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/transactional/welcome" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Transactional email deleted",
"deleted": {
"id": "txn_abc123",
"slug": "welcome",
"name": "Welcome Email",
"emailId": "email_abc123",
"emailDeleted": false
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: transactional:delete"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Transactional email \"welcome\" not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
# Get Transactional Email
Source: https://docs.sequenzy.com/api-reference/transactional/get
GET /api/v1/transactional/{idOrSlug}
Get transactional email details and body content by ID or slug
Get details of a transactional email template, including the linked email subject, preview text, body blocks, Style > Format, and available template variables.
## Path Parameters
Transactional email ID or slug
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/transactional/welcome" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"transactional": {
"id": "txn_abc123",
"name": "Welcome Email",
"slug": "welcome",
"emailId": "email_abc123",
"labels": ["Billing"],
"enabled": true,
"managedBy": "dashboard",
"subject": "Welcome, {{NAME}}",
"previewText": "Your account is ready.",
"emailPreset": "branded",
"blocks": [
{
"id": "block_123",
"type": "text",
"content": "
",
"variant": "html"
}
]
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
```
```json 404 theme={null}
{
"success": false,
"error": "Transactional email \"welcome\" not found"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
The `variables` array shows template variables extracted from the email
content. Use these when sending to customize the email.
`managedBy` is `code` for emails created by [sending direct content with
`trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code).
Their `subject`, `previewText` and `blocks` are a snapshot of a recent send,
refreshed at most every 10 minutes, and they cannot be sent by slug.
`emailPreset` is the email's Style > Format: `branded` (company logo and full
footer) or `minimal` (no logo, simple footer). It is derived from the stored
blocks and is reported the same way for sequence steps, campaigns, and saved
templates, so you can compare chrome across emails without rendering them. It
is `null` when the entire email is a single raw HTML block, which has no
Sequenzy format.
`transactional.labels` contains assigned company label names, or `[]` when unlabelled.
Insufficient API-key permissions return `403`. Label support uses the existing transactional read permission; no additional label scope is required.
# List Transactional Emails
Source: https://docs.sequenzy.com/api-reference/transactional/list
GET /api/v1/transactional
Search, filter, and sort transactional templates with their email subjects and delivery metrics.
List transactional email templates with the linked email subject, Style >
Format, and all-time delivery metrics. Open and click metrics exclude detected
machine engagement by default.
## Query Parameters
Match any company label name. Repeat `label` for multiple names, for example
`?label=Billing&label=Receipts`. Commas are literal parts of a name, unlike
campaign list comma shorthand. Combines with search and status. Unknown names
match no templates. Omit to include all labels. Blank or overlong names return
`400`; invalid query types return `422`.
Case-insensitive search across template name, API slug, and email
subject/title.
`all`, `active`, or `disabled`.
`date`, `sends`, `opens`, `open-rate`, `clicks`, or `ctr`.
`asc` or `desc`.
Include detected bots, scanners, previews, and privacy proxies in open and
click metrics.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/transactional?search=welcome&status=active&sort=open-rate&order=desc" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"transactional": [
{
"id": "txn_abc123",
"name": "Welcome Email",
"slug": "welcome",
"emailId": "email_abc123",
"subject": "Welcome to Acme",
"emailPreset": "branded",
"labels": ["Billing"],
"enabled": true,
"managedBy": "dashboard",
"stats": {
"sends": 100,
"deliveries": 98,
"opens": 72,
"clicks": 45,
"bounces": 2,
"openRate": 73.47,
"clickRate": 45.92
},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
},
{
"id": "txn_def456",
"name": "Password Reset",
"slug": "password-reset",
"emailId": "email_def456",
"subject": "Reset your password",
"emailPreset": "minimal",
"labels": ["Billing"],
"enabled": true,
"managedBy": "dashboard",
"stats": {
"sends": 40,
"deliveries": 40,
"opens": 30,
"clicks": 22,
"bounces": 0,
"openRate": 75,
"clickRate": 55
},
"createdAt": "2024-01-14T08:00:00Z",
"updatedAt": "2024-01-14T08:00:00Z"
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
`emailPreset` is each template's Style > Format: `branded` (company logo and
full footer), `minimal` (no logo, simple footer), or `null` when the entire
email is a single raw HTML block. Use it to check chrome consistency across
templates without fetching each one.
Each template includes `labels`, an array of assigned company label names, or `[]` when unlabelled.
Each template also includes `managedBy`: `dashboard` for emails designed in
Sequenzy, or `code` for emails created by [sending direct content with
`trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code).
A code-managed email's `subject` comes from a recent send, and its `stats`
count every send with that `trackAs`.
Insufficient API-key permissions return `403`. Label support uses the existing transactional read permission; no additional label scope is required.
# Send Email
Source: https://docs.sequenzy.com/api-reference/transactional/send
POST /api/v1/transactional/send
Send a transactional or consented marketing email
Send an email using a saved template (by slug) or custom content. The endpoint
defaults to transactional delivery. Use `emailType: "marketing"` for consented
lifecycle or promotional messages that need unsubscribe handling.
Use `emailType` to select delivery policy and `trackingSettings` to opt out of
open or click tracking for one send. `isMarketing` is not a request field.
A successful response means the email was accepted for background processing.
Transactional emails bypass subscriber unsubscribe and double opt-in status.
Marketing emails honor those statuses. Both modes suppress hard bounces and
spam complaints. A suppressed send is recorded instead of delivered. Use the
returned `emailSendId` with the email-send details endpoint to inspect the
final status and any provider failure reason.
## Safe retries
A caller-owned key for one logical email, up to 255 characters. Agents and
retrying workflows should generate the key before their first attempt and
reuse that exact key for every retry of the same request.
Sequenzy retains the key and the accepted send for 14 days. Retrying with the
same key and request returns the original response, includes
`Idempotent-Replayed: true`, and does not create another delivery. The stored
response is replayed as it was first returned, so a retry normally succeeds even
if the template has since been renamed or disabled, or the sending domain's
verification lapsed. The exception is a send that is still queued and whose
delivery job has to be rebuilt to recover it: that retry re-reads the template,
so it fails if the template was deleted or disabled in the meantime. Reusing the
key with different request content returns `409 IDEMPOTENCY_KEY_REUSED`.
Do not generate a new idempotency key inside each retry attempt. A new key
represents a new logical email and can create another delivery.
## Repeated junk-content rejection
If identical transactional content reaches many distinct recipients within a
short window, Sequenzy can review that content for junk/list testing. Identical
content or high volume alone does not establish abuse. Sending continues while
this review is pending or unavailable. A substantiated junk/list-testing verdict
can reject subsequent matching deliveries before they are sent.
The original API response still means accepted for background processing. Check
[Email Send Details](/api-reference/email-sends/get) for `status: "failed"` and an
`errorMessage` beginning `Transactional content rejected:`. This is a terminal
failure, not a queued hold: it is not automatically replayed when a decision
expires. Correct the content or contact support before deliberately submitting
a new logical send. Reusing the original idempotency key returns its original
acceptance response and does not resend the failed delivery.
Retrying a rejected delivery from the dashboard also fails without sending,
including after the review decision expires. Submit corrected content as a new
logical send instead.
The same accepted-send and delivery-status behavior applies to CLI and MCP sends.
This check does not pause your company or ban your account. Normal marketing
misuse is advisory for this check; existing phishing and sender-health protections
continue independently.
## Rate limits
Sends are metered separately from the rest of the API: 1,000 requests per
minute and 50 per second per API key, on top of the standard budget described
in [Rate Limiting](/api-reference/introduction#rate-limiting). Each request
sends to one recipient (or one `to` list), so a burst of confirmations does not
starve the subscriber or campaign calls made on the same key. A `429` response
carries `Retry-After`; queue and retry rather than dropping the send.
## Request Body
### Recipients
Recipient email address(es). Can be a single email string or an array of up to
50 emails. All recipients will receive the same email and can see each other
in the To header.
Carbon-copy email address(es), visible to every recipient. A single email
string or an array of up to 50. Addresses already present in `to` are removed.
Transactional sends only.
Blind-carbon-copy email address(es), hidden from the other recipients. A
single email string or an array of up to 50. Addresses already present in `to`
or `cc` are removed. Transactional sends only.
`cc` and `bcc` are echoed back in the response when the accepted send has
them, after the duplicates above are removed. The CLI exposes the same lists
as `sequenzy send ` with repeatable `--cc ` and
`--bcc ` flags, and the MCP `send_email` tool as the `to`, `cc`, and
`bcc` arguments.
### Option 1: Send via template
Canonical transactional template slug (use this OR direct content).
Compatibility alias for `slug`. Despite the field name, pass the saved
transactional email's API slug, not its database ID.
### Option 2: Send direct content
Email subject (required if no slug)
Canonical email HTML body (required if no template slug).
Compatibility alias for `body`, accepted with `subject` for direct sends.
Preview text
Names the email type, for example `weekly-report`, so every direct-content
send with the same value is counted under one code-managed transactional
email. See [Track emails rendered in your
code](#track-emails-rendered-in-your-code). Up to 255 ASCII letters, digits,
spaces and `. _ - : /`, including at least one letter or digit. Cannot be
combined with `slug` or `templateId`.
`slug`/`body` remain canonical. If you provide both `slug` and `templateId`,
or both `body` and `html`, the paired values must match. A differing pair is
rejected with `400` instead of silently choosing one.
### Common fields
Delivery policy. Use `transactional` for messages that are necessary to
complete a user-requested action or service. Use `marketing` for consented
lifecycle or promotional messages. Marketing mode requires exactly one `to`
recipient and does not support `cc` or `bcc`. It creates or links a minimal
subscriber, honors unsubscribe suppression, adds Sequenzy's standard marketing
footer, and emits the RFC 8058 `List-Unsubscribe` and `List-Unsubscribe-Post`
headers. The caller remains responsible for having a lawful basis or consent
to send the message.
Template variables for personalization. Values can be scalars, nested objects,
or arrays. Repeat blocks read arrays from paths such as `items`. Raw HTML
templates can also use subscriber/custom-attribute conditionals like
`{{#if subscriber.plan}}...{{else}}...{{/if}}` and
`{{#unless subscriber.plan}}...{{/unless}}`. If Sequenzy detects likely
variable issues before queueing, the successful response includes
`diagnostics` warnings. Missing required values do not block queueing; the
worker renders values without defaults as empty strings and continues sending.
For a single recipient, Sequenzy also matches an existing subscriber by
`subscriberExternalId` or email and uses the stored first and last name when
those values are omitted from `variables`. Explicit variables take precedence.
Variables are always HTML-escaped. To insert a trusted HTML value
unescaped (for example a prerendered order summary), prefix the tag with
`html.` in the template: `{{html.prerenderedHtml}}`. Injected HTML is
sanitized (scripts, event handlers, and dangerous URLs are stripped), only
applies in HTML text position, and must not contain end-user input. See
[Raw HTML variables](/concepts/transactional-emails#raw-html-variables).
`{{ viewInBrowserUrl }}` and `{{ VIEW_IN_BROWSER_URL }}` are reserved
variables. Sequenzy replaces either form with a hosted copy URL at send time,
so do not include them in `variables`. The hosted copy remains available while
the email body is retained (normally 14 days).
Customer-owned subscriber ID for single-recipient sends. If it matches an
existing subscriber, analytics, localization, and engagement attach to that
subscriber even if the delivery email changed. Sequenzy also stores this value
on the send, so outbound email webhooks include it as `external_id` even when
no subscriber record exists. Maximum length: 255 characters.
In marketing mode, this ID must either be unused or belong to the same email
address. Sequenzy rejects an identity mismatch instead of risking an
unsubscribe action against the wrong subscriber.
Custom from address. Format: `"Name "` or just `"email"`. The domain
must be verified for your account. If not verified, this field is silently
ignored and the default sender profile is used. When the address exactly
matches an existing sender identity (the display name disambiguates if
several share it), that identity - including its sending route - is used
for the send. Mutually exclusive with `senderProfileId` and `fromEmail`.
Existing sender profile ID. Sends this email as that verified From identity
(address and display name) instead of the company default or the saved
template's sender. Get IDs from [List Sender
Profiles](/api-reference/sender-profiles/list). Mutually exclusive with
`fromEmail` and `from`.
From address of an existing verified sender profile in this company. Mutually
exclusive with `senderProfileId` and `from`. If the address has several
identities, pass `fromName` to choose one (or `senderProfileId`). This field
does not create a new sender profile.
Display name of an existing verified sender identity on `fromEmail`. Use this
when one address has several identities, for example StudyBoost vs StudyBoost
News. Requires `fromEmail`. Mutually exclusive with `senderProfileId` and
`from`. Does not create a new sender profile.
Reply-to address. Format: `"Name "` or just `"email"`. Can be any valid
email address. Mutually exclusive with `replyProfileId`. Pair a bare address
with `replyToName` instead of embedding the name.
When reply tracking is disabled, this is sent as the email's `Reply-To` header.
When reply tracking is enabled, Sequenzy sends a unique trackable `Reply-To`
address instead and stores this value as the forwarding destination for replies.
Display name for a bare `replyTo` address. Requires `replyTo`. Mutually
exclusive with `replyProfileId`. Does not create a reply profile.
Existing reply profile ID. Sends this email with that Reply-To identity
instead of the company default or the saved template's reply profile. Get IDs
from [List Sender Profiles](/api-reference/sender-profiles/list). Mutually
exclusive with `replyTo`.
`from` and `replyTo` control message identity, not the outbound transport. A
mailbox or alias hosted by Hostinger or another provider can be used as `from`
after its domain is verified, and as `replyTo` without domain verification.
Sequenzy still performs delivery through SES or Sequenzy's MTA; custom
outbound SMTP relays are not supported.
Direct-content sends without a reply identity override prefer the effective
sending domain's default reply profile, then the company's default reply
profile, then the first reply profile in the company. Sequenzy retains that
resolved address in `originalReplyTo`. When reply tracking is disabled, it is
sent as the Reply-To header; when reply tracking is enabled, Sequenzy sends a
unique trackable Reply-To and uses `originalReplyTo` as the forwarding
destination. Template sends prefer the request override, then the template
reply profile, then the same domain, company and first-profile fallback order.
File attachments to include with the email. Maximum 10 attachments, 15MB total.
Each attachment object has:
* `filename` (required): The name of the file as it will appear to the recipient
* `content`: Base64-encoded file content (use this OR `path`)
* `path`: URL to fetch the file from (use this OR `content`)
* `contentId`: Embeds the file as an inline image instead of attaching it. Reference it from your HTML as ``
* `contentType`: MIME type override. Defaults to the type inferred from `filename`
You must provide either `content` or `path`, but not both.
Per-send tracking opt-outs. When a field is omitted, the corresponding
Transactional API default from **Settings → Tracking** applies. Set a field
to `false` to disable that tracking for this send only. These fields can only
opt out; they cannot enable tracking disabled by either an account-wide or
Transactional API setting.
Set `false` to skip link rewriting so the original URLs are delivered
unchanged, which keeps iOS/Android universal links and deep links working
when the click-tracking redirect domain would otherwise break them.
Set `false` to skip the open-tracking pixel for this send only.
Extra email headers as an object of header names to string values, for
example `{ "X-Entity-Ref-ID": "digest-42" }`. Up to 50 headers are applied.
Names may contain letters, digits and hyphens (up to 100 characters). Values
must be one line of printable ASCII, and each `Name: value` line must fit in
998 characters.
* **Your own unsubscribe link:** on a `transactional` send you can set
`List-Unsubscribe`, and `List-Unsubscribe-Post` with the exact value
`List-Unsubscribe=One-Click` for RFC 8058 one-click unsubscribe.
`List-Unsubscribe-Post` is only applied together with `List-Unsubscribe`.
Unsubscribes through your link go to your system; Sequenzy does not suppress
the address.
* **Marketing mode:** Sequenzy always adds its own signed unsubscribe headers
and bulk signals, so `List-Unsubscribe`, `List-Unsubscribe-Post`,
`Precedence` and `Feedback-ID` are not applied.
* **Headers Sequenzy manages** are never applied: `From`, `Sender`, `To`,
`Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version`,
`Return-Path`, `Received`, `Received-SPF`, `Delivered-To`,
`DKIM-Signature`, `Authentication-Results`, and names starting with
`Content-`, `X-Sequenzy-`, `X-SES-`, `ARC-` or `Resent-`. Use the dedicated request fields for sender, recipients,
reply-to and subject.
A header that cannot be applied never fails the request. The send is queued
without it and the response lists it in `ignoredHeaders` with a reason. Header
names are matched case-insensitively. When names differ only in case, only the
first one is considered, so later ones are ignored even if the first one was
not applied. Headers apply to every automatic delivery attempt of the
queued send. A retry of a failed delivery, from the dashboard or during
Sequenzy's outage recovery, resends the stored message without them.
## Example: Send via template
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: welcome-user-123" \
-H "Content-Type: application/json" \
-d '{
"to": "user@sequenzy.com",
"subscriberExternalId": "user_123",
"slug": "welcome",
"variables": {
"NAME": "John",
"LOGIN_URL": "https://sequenzy.com/sign-in"
}
}'
```
## Example: Send a consented lifecycle message
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subscriberExternalId": "user_123",
"slug": "trial-reminder",
"emailType": "marketing",
"variables": {
"firstName": "Ana",
"trialEndsAt": "2026-07-24"
}
}'
```
Sequenzy adds the visible unsubscribe footer and signed one-click headers. Do
not supply an unsubscribe URL in the request. `List-Unsubscribe` headers in
`headers` are not applied to a marketing send and are listed in
`ignoredHeaders`.
The successful queue response includes the accepted policy:
```json theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"emailType": "marketing",
"to": "user@example.com"
}
```
## Example: Send array data to a repeat block
If a template contains a repeat block with source `items` and item alias
`item`, child blocks can use merge tags like `{{item.title}}` and
`{{item.description}}`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"slug": "order-confirmation",
"variables": {
"NAME": "John",
"items": [
{
"title": "Pro plan",
"description": "Monthly subscription"
},
{
"title": "Priority support",
"description": "Added to your account"
}
]
}
}'
```
## Example: Send to multiple recipients
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["user1@example.com", "user2@example.com"],
"slug": "invoice",
"variables": {
"INVOICE_NUMBER": "INV-2024-001",
"AMOUNT": "$500.00"
}
}'
```
## Example: Send direct content
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@sequenzy.com",
"subject": "Your order is confirmed",
"body": "
Thank you, {{NAME}}!
Order #{{ORDER_ID}} confirmed.
",
"variables": {
"NAME": "John",
"ORDER_ID": "12345"
}
}'
```
The same direct send can use the `html` compatibility alias:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@sequenzy.com",
"subject": "Connection test",
"html": "
Connected.
"
}'
```
## Track emails rendered in your code
Some emails are easier to build in your own code, like a weekly summary with
per-account numbers. Send them as direct content and add `trackAs` to review
their performance in one place:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@sequenzy.com",
"subject": "Your week in review",
"html": "
Your week
3 new subscribers, 42% open rate.
",
"trackAs": "weekly-report"
}'
```
* The first send with a new `trackAs` creates a transactional email with
`managedBy: "code"`. Later sends with the same value link to it. The value is
normalized like a slug, so `Weekly Report` and `weekly-report` are the same
email. Its display name starts as a title (`weekly-report` becomes `Weekly
report`) and can be renamed without affecting tracking. It may use up to 255 ASCII letters, digits, spaces and `. _ - : /`;
anything else, including an empty value, returns `400 INVALID_TRACK_AS`.
* Sends, deliveries, opens, clicks and bounces aggregate on it like on a saved
template. It is listed by `GET /api/v1/transactional` and in the dashboard,
where you can review its stats and a snapshot of recent content. The snapshot
refreshes at most every 10 minutes and is stored before variable substitution.
* Your code stays the source of truth. A code-managed email cannot be sent by
`slug`, and updates only accept `name`, `enabled` and `labels`.
* Disabling it rejects further sends with that `trackAs` (`400
TRACKED_EMAIL_DISABLED`) until you enable it again. Sends already queued
still go out. Deleting it also deletes its content snapshot; past sends stay
in email-send history, and the next send creates a new email with fresh
stats.
* A value that matches the slug of a template managed in the dashboard is
rejected (`400 TRACK_AS_MANAGED_IN_DASHBOARD`). Each company can have up to 100
code-managed emails (`400 CODE_MANAGED_LIMIT_REACHED`), so use a stable name
for an email type and never include per-recipient values.
* The email is created after the send passes request validation, so an invalid
send does not create it. A send that fails later, such as an
`Idempotency-Key` conflict, can leave the new email with no sends. The
response includes a `transactional` object with its `id`, `slug` and `name`.
## Example: Send with custom from and reply-to
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"slug": "order-confirmation",
"variables": {
"NAME": "John",
"ORDER_ID": "12345"
},
"from": "Notifications ",
"replyTo": "Support "
}'
```
## Example: Send with URL-based attachment
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"slug": "invoice",
"variables": {
"INVOICE_NUMBER": "INV-2024-001"
},
"attachments": [
{
"filename": "invoice.pdf",
"path": "https://example.com/invoices/INV-2024-001.pdf"
}
]
}'
```
## Example: Send with Base64 attachment
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Your report is ready",
"body": "
Please find your report attached.
",
"attachments": [
{
"filename": "report.csv",
"content": "TmFtZSxFbWFpbCxTdGF0dXMKSm9obixqb2huQGV4YW1wbGUuY29tLEFjdGl2ZQ=="
}
]
}'
```
## Example: Send with an inline (CID) image
Set `contentId` and reference it from your HTML. The image is sent as a
`multipart/related` part rather than a file attachment, which is the only form
Gmail renders - it drops `data:` URI images.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Your ticket",
"body": "
Scan at the gate:
",
"attachments": [
{
"filename": "ticket-qr.png",
"contentId": "ticket-qr",
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
}
]
}'
```
If nothing in the HTML references `cid:ticket-qr`, the file is sent as a normal
attachment instead.
## Example: Use your own unsubscribe link
Keep your own per-email-type unsubscribe on a transactional send by passing
your one-click URL in `headers`:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"slug": "weekly-digest",
"headers": {
"List-Unsubscribe": "",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}
}'
```
Your endpoint must accept the RFC 8058 `POST` that mailbox providers send to
this URL and stop that email type for the recipient.
## Responses
```json 200 Template (single recipient) theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"to": "user@sequenzy.com",
"transactional": {
"id": "txn_abc123",
"slug": "welcome",
"name": "Welcome Email"
}
}
```
```json 200 Template with diagnostics theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"to": "user@sequenzy.com",
"transactional": {
"id": "txn_abc123",
"slug": "password-reset",
"name": "Password Reset"
},
"diagnostics": {
"status": "warning",
"message": "Email was queued. A required variable is missing and will render as an empty string.",
"missingRequiredVariables": [
{
"name": "RESET_URL",
"lookupName": "reset_url",
"message": "Email was queued. Required variable \"RESET_URL\" is missing and will render as an empty string.",
"usedIn": [
{
"surface": "block",
"blockId": "reset-button",
"blockType": "button",
"field": "url"
}
],
"suggestions": ["resetpasswordlink"]
}
],
"unusedVariables": [
{
"name": "resetpasswordlink",
"message": "Variable \"resetpasswordlink\" was provided but is not used by the email template.",
"suggestions": ["reset_url"]
}
]
}
}
```
```json 200 Template (multiple recipients) theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"to": ["user1@example.com", "user2@example.com"],
"transactional": {
"id": "txn_abc123",
"slug": "invoice",
"name": "Invoice Email"
}
}
```
```json 200 Direct theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"to": "user@sequenzy.com"
}
```
```json 200 With ignored headers theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"emailType": "marketing",
"to": "user@sequenzy.com",
"ignoredHeaders": [
{ "name": "List-Unsubscribe", "reason": "managed_in_marketing_mode" },
{ "name": "From", "reason": "reserved" }
]
}
```
```json 200 Direct with trackAs theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"jobId": "job_xyz789",
"to": "user@sequenzy.com",
"transactional": {
"id": "txn_weekly123",
"slug": "weekly-report",
"name": "Weekly report"
}
}
```
A successful response means Sequenzy accepted the send for background
processing. Rendering or provider failures after acceptance are recorded on the
email send as failed delivery attempts. Check
`GET /api/v1/email-sends/{emailSendId}` for `status` and `errorMessage`. The
legacy `jobId` response field remains for compatibility but is not the public
delivery-status contract. If `diagnostics.status` is `warning`,
the send is still queued. The warnings identify missing values that will render
as empty strings or variables that look unused.
`ignoredHeaders` is present only when at least one entry in `headers` was not
applied. Each item has the header `name` as you sent it and a `reason`:
| Reason | Meaning |
| - | - |
| `invalid_headers` | `headers` was not an object. `name` is `*`. |
| `invalid_name` | The name has characters other than letters, digits and hyphens, or is too long. |
| `invalid_value` | The value is not a non-empty single line of printable ASCII, is too long, or is a `List-Unsubscribe-Post` other than `List-Unsubscribe=One-Click`. |
| `duplicate` | The same name, in any case, appeared earlier. |
| `reserved` | Sequenzy manages this header. |
| `managed_in_marketing_mode` | Marketing sends use Sequenzy's own value. |
| `requires_list_unsubscribe` | `List-Unsubscribe-Post` was sent without an applied `List-Unsubscribe`. |
| `too_many_headers` | More than 50 headers would have been applied. |
The idempotency request hash includes only the headers that were applied, so a
replay reports the ignored headers of the retried request. A retry that adds
`headers` to a key first used without any replays the original send instead of
returning `409`: requests made before `headers` was supported were stored
without them.
If queue infrastructure is temporarily unavailable after a request has been
reserved, the endpoint returns `503 TRANSACTIONAL_ENQUEUE_UNAVAILABLE` with
`retryable: true`. Retry the complete request with the same `Idempotency-Key`;
Sequenzy repairs the same reserved send instead of creating a second one.
```json 409 Idempotency key reused theme={null}
{
"success": false,
"error": "Idempotency-Key was already used with a different request",
"code": "IDEMPOTENCY_KEY_REUSED"
}
```
```json 503 Retry with the same key theme={null}
{
"success": false,
"error": "Email send is reserved but could not be queued; retry with the same Idempotency-Key",
"code": "TRANSACTIONAL_ENQUEUE_UNAVAILABLE",
"retryable": true
}
```
```json 400 Missing fields theme={null}
{
"success": false,
"error": "Either 'slug'/'templateId' OR both 'subject' and 'body'/'html' are required"
}
```
```json 400 Disabled theme={null}
{
"success": false,
"error": "Transactional email \"welcome\" is disabled"
}
```
```json 400 trackAs with a template theme={null}
{
"success": false,
"error": "trackAs only applies to sends with subject and body/html. Saved templates already track their own stats.",
"code": "TRACK_AS_REQUIRES_DIRECT_CONTENT"
}
```
```json 400 Invalid trackAs theme={null}
{
"success": false,
"error": "trackAs must be 1-255 characters of ASCII letters, digits, spaces or . _ - : / and include a letter or digit",
"code": "INVALID_TRACK_AS"
}
```
```json 400 trackAs matches a dashboard template theme={null}
{
"success": false,
"error": "trackAs \"welcome\" matches a transactional email managed in the dashboard. Send it with slug, or choose a different trackAs.",
"code": "TRACK_AS_MANAGED_IN_DASHBOARD"
}
```
```json 400 Code-managed email disabled theme={null}
{
"success": false,
"error": "Transactional email \"weekly-report\" is disabled",
"code": "TRACKED_EMAIL_DISABLED"
}
```
```json 400 Code-managed limit theme={null}
{
"success": false,
"error": "This workspace already tracks 100 code-managed emails. Reuse an existing trackAs value or delete unused ones; trackAs should name an email type, not include per-recipient values.",
"code": "CODE_MANAGED_LIMIT_REACHED"
}
```
```json 400 Code-managed email sent by slug theme={null}
{
"success": false,
"error": "Transactional email \"weekly-report\" is managed in code. Send subject and body with trackAs \"weekly-report\" instead of slug."
}
```
```json 400 No sender theme={null}
{
"success": false,
"error": "No sender profile configured. Please create a sender profile first."
}
```
```json 400 Invalid attachment theme={null}
{
"success": false,
"error": "Invalid attachments: Attachment[0]: Attachment must have either 'content' (Base64) or 'path' (URL)"
}
```
```json 400 Attachment too large theme={null}
{
"success": false,
"error": "Attachment \"large-file.pdf\" exceeds maximum size of 15MB"
}
```
```json 400 Total size exceeded theme={null}
{
"success": false,
"error": "Total attachment size exceeds 15MB limit"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Transactional email with slug \"welcome\" not found"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
Emails are queued for background processing. Track the returned `emailSendId`
with `GET /api/v1/email-sends/{emailSendId}`. For single-recipient sends
without `cc` or `bcc`, `EMAIL` resolves from the recipient address. If the
recipient matches a stored subscriber, `FIRST_NAME`, `LAST_NAME`, and `NAME`
use the saved profile values when the request omits them. Passing `name`,
`firstName`, `lastName`, `FIRST_NAME`, or `LAST_NAME` in `variables` overrides
the corresponding saved value.
For aggregate Send API open and click rates, call `GET
/api/v1/metrics?period=30d&emailType=transactional`, run `sequenzy stats --email-type transactional --period 30d`, or call the MCP `get_stats` tool
with `emailType: "transactional"`.
When using multiple `to` recipients, all recipient addresses are visible to
each other. Use single-recipient sends when recipients should not see each
other.
Multi-recipient sends produce a single email send record. The full recipient
list (to/cc/bcc) is stored on the record and returned as
`additionalRecipients` from the email-sends API. Opens and clicks are tracked
for the message as a whole, not per recipient.
# Update Transactional Email
Source: https://docs.sequenzy.com/api-reference/transactional/update
PATCH /api/v1/transactional/{idOrSlug}
Update transactional email metadata or body content
Update a transactional email by ID or slug. You can update metadata like `name` and `enabled`, or replace the linked email body with `html` or `blocks`.
Code-managed emails (`managedBy: "code"`, created by [sends with
`trackAs`](/api-reference/transactional/send#track-emails-rendered-in-your-code))
accept only `name`, `enabled` and `labels`. Their content comes from each send,
so `subject`, `previewText`, `html` and `blocks` return `400`. Set `enabled` to
`false` to reject further sends with that `trackAs`.
## Path Parameters
Transactional email ID or slug
## Request
Company label names, trimmed and deduplicated. Missing names are created in
your company. Each name must be non-empty and at most 255 characters. Send
`[]` to clear assignments. Omit to keep existing labels. `null`, blank names,
and non-string entries are rejected.
Transactional email name
Whether this transactional email can be sent
Email subject line
Email preview text. Send `null` to clear it; omit the field to keep the
current value.
Raw HTML body. Provide either `html` or `blocks`, not both.
Sequenzy email blocks. Provide either `blocks` or `html`, not both. Put block
styling under `styles`; top-level style keys like `backgroundColor`,
`backgroundOpacity`, `textColor`, `textAlign`, `borderColor`, `borderWidth`,
and `borderRadius` are normalized into `styles`.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/transactional/welcome" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Welcome, {{NAME}}",
"html": "
",
"variant": "html"
}
],
"variables": ["NAME", "COMPANY_NAME"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:35:00Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Provide at least one of name, enabled, subject, previewText, html, blocks, or labels."
}
```
```json 400 Code-managed content theme={null}
{
"success": false,
"error": "This transactional email is managed in code. Its content comes from each send; update name, enabled or labels instead."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"error": "Transactional email \"welcome\" not found"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Internal server error"
}
```
Label names appear in the response's `transactional.labels` array. Invalid
label names return `400`; invalid label types return `422`. Retrying the same
label replacement does not duplicate company labels or send an email. After
an uncertain update response, read the template before retrying. Label-only
updates preserve email content and translations.
Insufficient API-key permissions return `403`. Label support uses the existing transactional write permission; no additional label scope is required.
# Create Warehouse Connection
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-create
POST /api/v1/warehouse-connections
Connect Snowflake, BigQuery, Redshift or Postgres
Connects a data warehouse. Sequenzy runs `SELECT 1` with the credentials before saving; if it fails, nothing is saved and the response contains the warehouse's reason. Requires `warehouse:write`.
Use a user that can only read the tables you want to sync. See [Warehouse Sync](/integrations/warehouse-sync) for setup steps per warehouse.
## Request
Display name, up to 255 characters.
`snowflake`, `bigquery`, `redshift` or `postgres`.
Non-secret settings. **snowflake**: `account` (for example `myorg-myaccount`
or `xy12345.us-east-1`), `username`, `warehouse`, `database`, optional
`schema` and `role`. **bigquery**: `projectId`, optional `location` (for
example `US`). **redshift** and **postgres**: `host`, `database`, `username`,
optional `port` (default `5439` or `5432`) and `sslMode` (`require`, the
default, or `verify-full`). The host must resolve to a public address.
Secrets, stored encrypted and never returned. **snowflake**: `privateKey`
(PKCS#8 PEM for key-pair authentication) and optional `privateKeyPassphrase`.
**bigquery**: `serviceAccountJson`, the contents of the service account key
file. **redshift** and **postgres**: `password`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/warehouse-connections" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Analytics Postgres",
"provider": "postgres",
"config": { "host": "db.example.com", "database": "analytics", "username": "sequenzy_reader" },
"credentials": { "password": "..." }
}'
```
```json 201 theme={null}
{
"success": true,
"connection": {
"id": "wconn_abc123",
"name": "Snowflake prod",
"provider": "snowflake",
"config": {
"account": "myorg-myaccount",
"username": "SEQUENZY_READER",
"warehouse": "ANALYTICS_WH",
"database": "PROD",
"schema": "PUBLIC",
"role": "SEQUENZY_READ"
},
"status": "connected",
"lastTestedAt": "2026-09-30T12:00:00.000Z",
"lastError": null,
"syncCount": 1,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "Snowflake rejected the credentials: JWT token is invalid. Check the account, user, and that the user's RSA_PUBLIC_KEY matches this private key."
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "Host must resolve to a public internet address."
}
```
# Delete Warehouse Connection
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-delete
DELETE /api/v1/warehouse-connections/{id}
Delete a warehouse connection
Deletes a connection and its stored credentials. Delete its syncs first. Requires `warehouse:delete`.
Connection ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/warehouse-connections/wconn_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"connectionId": "wconn_abc123"
}
```
```json 409 theme={null}
{
"success": false,
"code": "connection_in_use",
"error": "This connection is used by 1 sync. Delete it first."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse connection not found."
}
```
# Get Warehouse Connection
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-get
GET /api/v1/warehouse-connections/{id}
Get one warehouse connection
Returns one connection. Fields match [List Warehouse Connections](/api-reference/warehouse/connections-list). Requires `warehouse:read`.
Connection ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/warehouse-connections/wconn_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"connection": {
"id": "wconn_abc123",
"name": "Snowflake prod",
"provider": "snowflake",
"config": {
"account": "myorg-myaccount",
"username": "SEQUENZY_READER",
"warehouse": "ANALYTICS_WH",
"database": "PROD",
"schema": "PUBLIC",
"role": "SEQUENZY_READ"
},
"status": "connected",
"lastTestedAt": "2026-09-30T12:00:00.000Z",
"lastError": null,
"syncCount": 1,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse connection not found."
}
```
# List Warehouse Connections
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-list
GET /api/v1/warehouse-connections
List connected warehouses
Returns every warehouse connection with its status, last test and number of syncs. Credentials are never returned. Requires `warehouse:read`; marketers cannot use warehouse routes.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/warehouse-connections" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`snowflake`, `bigquery`, `redshift` or `postgres`.
Non-secret settings. BigQuery connections also show `serviceAccountEmail`.
`connected`, or `error` when the last test failed.
Reason from the warehouse for the last failed test.
```json 200 theme={null}
{
"success": true,
"connections": [
{
"id": "wconn_abc123",
"name": "Snowflake prod",
"provider": "snowflake",
"config": {
"account": "myorg-myaccount",
"username": "SEQUENZY_READER",
"warehouse": "ANALYTICS_WH",
"database": "PROD",
"schema": "PUBLIC",
"role": "SEQUENZY_READ"
},
"status": "connected",
"lastTestedAt": "2026-09-30T12:00:00.000Z",
"lastError": null,
"syncCount": 1,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
]
}
```
```json 403 theme={null}
{
"success": false,
"error": "API key is missing required scope: warehouse:read"
}
```
# Preview Warehouse Query
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-preview
POST /api/v1/warehouse-connections/{id}/preview
Run a query with a 20-row limit to see its columns
Runs a query with a 20-row limit and returns its columns and rows, so you can build a [sync mapping](/api-reference/warehouse/syncs-create). Only a single `SELECT` or `WITH` statement is accepted; it runs as a subquery. Requires `warehouse:write`.
Connection ID.
The query, up to 100,000 characters.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/warehouse-connections/wconn_abc123/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "select email, plan, updated_at from analytics.customers" }'
```
```json 200 theme={null}
{
"success": true,
"columns": [
{
"name": "EMAIL",
"type": "TEXT"
},
{
"name": "PLAN",
"type": "TEXT"
},
{
"name": "UPDATED_AT",
"type": "TIMESTAMP_NTZ"
}
],
"rows": [
{
"EMAIL": "ada@example.com",
"PLAN": "pro",
"UPDATED_AT": "2026-09-30T11:58:02.000Z"
}
]
}
```
```json 400 theme={null}
{
"success": false,
"code": "invalid_query",
"error": "Query must be a single SELECT statement (WITH clauses are fine)."
}
```
```json 400 theme={null}
{
"success": false,
"code": "query_failed",
"error": "Snowflake: SQL compilation error: Object 'ANALYTICS.CUSTOMERS' does not exist or not authorized."
}
```
# Test Warehouse Connection
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-test
POST /api/v1/warehouse-connections/{id}/test
Run a test query with the stored credentials
Runs `SELECT 1` with the stored credentials. The result is saved on the connection: `status` becomes `connected`, or `error` with `lastError`. Requires `warehouse:write`.
Connection ID.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/warehouse-connections/wconn_abc123/test" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"connection": {
"id": "wconn_abc123",
"name": "Snowflake prod",
"provider": "snowflake",
"config": {
"account": "myorg-myaccount",
"username": "SEQUENZY_READER",
"warehouse": "ANALYTICS_WH",
"database": "PROD",
"schema": "PUBLIC",
"role": "SEQUENZY_READ"
},
"status": "connected",
"lastTestedAt": "2026-09-30T12:00:00.000Z",
"lastError": null,
"syncCount": 1,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "Could not connect to the database. Check the host and port, and allow connections from Sequenzy in your firewall."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse connection not found."
}
```
# Update Warehouse Connection
Source: https://docs.sequenzy.com/api-reference/warehouse/connections-update
PATCH /api/v1/warehouse-connections/{id}
Rename, change settings or rotate credentials
Updates a connection. `config` is merged with the saved settings. Settings and credential changes run a new test query before saving; if it fails, nothing changes. Requires `warehouse:write`.
Connection ID.Settings to change, for example `{ "port": 6543 }`.Replacement credentials. Omit to keep the stored ones.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/warehouse-connections/wconn_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "credentials": { "password": "new-password" } }'
```
```json 200 theme={null}
{
"success": true,
"connection": {
"id": "wconn_abc123",
"name": "Snowflake prod",
"provider": "snowflake",
"config": {
"account": "myorg-myaccount",
"username": "SEQUENZY_READER",
"warehouse": "ANALYTICS_WH",
"database": "PROD",
"schema": "PUBLIC",
"role": "SEQUENZY_READ"
},
"status": "connected",
"lastTestedAt": "2026-09-30T12:00:00.000Z",
"lastError": null,
"syncCount": 1,
"createdAt": "2026-09-30T12:00:00.000Z",
"updatedAt": "2026-09-30T12:00:00.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "connection_failed",
"error": "The database rejected the username or password."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse connection not found."
}
```
# Create Warehouse Sync
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-create
POST /api/v1/warehouse-syncs
Import contacts or events from a warehouse query on a schedule
Creates a sync. Each run executes the query and sends only rows that changed since they were last sent:
* **subscribers** syncs create and update contacts through the same pipeline as [imports](/api-reference/subscribers/import-create), with duplicate strategy `overwrite`: mapped values replace stored ones, empty values never clear data, tags are only added, and unsubscribed contacts are never resubscribed.
* **events** syncs record events on contacts that already exist. Events older than an hour are stored as history and never start automations.
The mapping and cursor column are checked against the query's real columns, and the first run starts right away.
Requires `warehouse:write` and `subscribers:write`. Event syncs also need `events:write`. `triggerAutomations: true` or `optInMode: "double_opt_in"` need `automations:trigger`.
## Request
`subscribers` or `events`.
A single `SELECT` or `WITH` statement.
Column names as the query returns them (Snowflake returns unquoted names in upper case).
**subscribers**: `emailColumn` (or `phoneColumn` for SMS-only contacts), `externalIdColumn`, `firstNameColumn`, `lastNameColumn`, `tagsColumn` (array or comma-separated string) and `attributes`, a list of `{ "column", "attribute" }` pairs.
**events**: `emailColumn` or `externalIdColumn` to find the contact, `eventNameColumn` or a fixed `eventName`, `eventIdColumn` (stable ID; derived from the row when omitted), `occurredAtColumn` (defaults to the sync time) and `properties`, a list of `{ "column", "property" }` pairs.
A column that grows when a row changes, such as `updated_at`. Each run reads
only rows at or after the last value it saw. Without a cursor, every run reads
the whole result (up to 2,000,000 rows) and skips unchanged rows.
`every_15_minutes`, `hourly`, `daily`, `weekly` or `manual`.
Subscriber syncs: lists that contacts the sync creates join. Existing contacts
keep their list memberships.
Subscriber syncs: `confirmed` for contacts whose consent is already verified,
or `double_opt_in` to email new contacts a confirmation first.
Let new contacts, or events from the last hour, start automations.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/warehouse-syncs" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [{ "column": "PLAN", "attribute": "plan" }]
},
"cursorColumn": "UPDATED_AT",
"frequency": "hourly"
}'
```
```json 201 theme={null}
{
"success": true,
"sync": {
"id": "wsync_abc123",
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [
{
"column": "PLAN",
"attribute": "plan"
}
]
},
"cursorColumn": "UPDATED_AT",
"cursorValue": null,
"listIds": [],
"optInMode": "confirmed",
"triggerAutomations": false,
"frequency": "hourly",
"isEnabled": true,
"fullResyncRequested": false,
"status": "queued",
"nextRunAt": "2026-09-30T13:00:00.000Z",
"lastRunAt": "2026-09-30T12:00:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:41.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"latestRun": null,
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-09-30T12:00:41.000Z"
}
}
```
```json 400 theme={null}
{
"success": false,
"code": "invalid_mapping",
"error": "emailColumn \"MAIL\" is not a column of the query. Available columns: EMAIL, FIRST_NAME, PLAN, UPDATED_AT."
}
```
```json 403 theme={null}
{
"success": false,
"code": "missing_scope",
"error": "Starting automations or sending double opt-in emails from a sync requires the automations:trigger permission."
}
```
# Delete Warehouse Sync
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-delete
DELETE /api/v1/warehouse-syncs/{id}
Delete a warehouse sync
Deletes a sync and its run history. Scheduled runs stop; contacts and events already imported stay. Requires `warehouse:delete`.
Sync ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/warehouse-syncs/wsync_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"syncId": "wsync_abc123"
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse sync not found."
}
```
# Get Warehouse Sync
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-get
GET /api/v1/warehouse-syncs/{id}
Get one warehouse sync
Returns one sync with its latest run. Requires `warehouse:read`.
Sync ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/warehouse-syncs/wsync_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"sync": {
"id": "wsync_abc123",
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [
{
"column": "PLAN",
"attribute": "plan"
}
]
},
"cursorColumn": "UPDATED_AT",
"cursorValue": "2026-09-30T11:58:02.000Z",
"listIds": [],
"optInMode": "confirmed",
"triggerAutomations": false,
"frequency": "hourly",
"isEnabled": true,
"fullResyncRequested": false,
"status": "idle",
"nextRunAt": "2026-09-30T13:00:00.000Z",
"lastRunAt": "2026-09-30T12:00:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:41.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"latestRun": {
"id": "wrun_1",
"syncId": "wsync_abc123",
"status": "succeeded",
"isManual": false,
"isFullResync": false,
"rowsRead": 1204,
"rowsUnchanged": 1100,
"rowsSynced": 98,
"rowsSkipped": 6,
"rowsFailed": 0,
"errorSamples": [
{
"row": 17,
"reason": "Missing email"
}
],
"subscriberImportIds": [
"imp_1"
],
"imports": [
{
"id": "imp_1",
"status": "completed",
"totalRows": 98,
"processedCount": 98,
"addedCount": 12,
"updatedCount": 86,
"skippedCount": 0,
"failedCount": 0
}
],
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:41.000Z"
},
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-09-30T12:00:41.000Z"
}
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse sync not found."
}
```
# List Warehouse Syncs
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-list
GET /api/v1/warehouse-syncs
List warehouse syncs with their latest run
Returns every sync with its schedule, status, cursor position and latest run, including the progress of the subscriber imports it queued. Requires `warehouse:read`.
Only syncs of this connection.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/warehouse-syncs" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
`subscribers` or `events`.
`idle`, `queued`, `running`, or `failed` (a retry is scheduled with
backoff).
Rows with the cursor column at or after this value are read next.
A full resync runs next.
Why the last run failed, from the warehouse or your query. Problems on
Sequenzy's side show a generic message.
The most recent run. See [List Runs](/api-reference/warehouse/syncs-runs).
```json 200 theme={null}
{
"success": true,
"syncs": [
{
"id": "wsync_abc123",
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [
{
"column": "PLAN",
"attribute": "plan"
}
]
},
"cursorColumn": "UPDATED_AT",
"cursorValue": "2026-09-30T11:58:02.000Z",
"listIds": [],
"optInMode": "confirmed",
"triggerAutomations": false,
"frequency": "hourly",
"isEnabled": true,
"fullResyncRequested": false,
"status": "idle",
"nextRunAt": "2026-09-30T13:00:00.000Z",
"lastRunAt": "2026-09-30T12:00:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:41.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"latestRun": {
"id": "wrun_1",
"syncId": "wsync_abc123",
"status": "succeeded",
"isManual": false,
"isFullResync": false,
"rowsRead": 1204,
"rowsUnchanged": 1100,
"rowsSynced": 98,
"rowsSkipped": 6,
"rowsFailed": 0,
"errorSamples": [
{
"row": 17,
"reason": "Missing email"
}
],
"subscriberImportIds": [
"imp_1"
],
"imports": [
{
"id": "imp_1",
"status": "completed",
"totalRows": 98,
"processedCount": 98,
"addedCount": 12,
"updatedCount": 86,
"skippedCount": 0,
"failedCount": 0
}
],
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:41.000Z"
},
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-09-30T12:00:41.000Z"
}
]
}
```
# Run Warehouse Sync
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-run
POST /api/v1/warehouse-syncs/{id}/run
Run a sync now, optionally as a full resync
Queues a run now. Only one run per sync executes at a time; a request made while a run is in progress runs right after it. Requires `warehouse:write` and `subscribers:write` (plus `events:write` for event syncs). A full resync of a sync that starts automations or sends double opt-in emails also needs `automations:trigger`.
Sync ID.
Forget which rows were sent and the cursor position, and send every row again.
Use it after fixing data in your warehouse without touching the cursor column.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/warehouse-syncs/wsync_abc123/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fullResync": true }'
```
```json 200 theme={null}
{
"success": true,
"queued": true,
"sync": {
"id": "wsync_abc123",
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [
{
"column": "PLAN",
"attribute": "plan"
}
]
},
"cursorColumn": "UPDATED_AT",
"cursorValue": "2026-09-30T11:58:02.000Z",
"listIds": [],
"optInMode": "confirmed",
"triggerAutomations": false,
"frequency": "hourly",
"isEnabled": true,
"fullResyncRequested": true,
"status": "queued",
"nextRunAt": "2026-09-30T13:00:00.000Z",
"lastRunAt": "2026-09-30T12:00:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:41.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"latestRun": {
"id": "wrun_1",
"syncId": "wsync_abc123",
"status": "succeeded",
"isManual": false,
"isFullResync": false,
"rowsRead": 1204,
"rowsUnchanged": 1100,
"rowsSynced": 98,
"rowsSkipped": 6,
"rowsFailed": 0,
"errorSamples": [
{
"row": 17,
"reason": "Missing email"
}
],
"subscriberImportIds": [
"imp_1"
],
"imports": [
{
"id": "imp_1",
"status": "completed",
"totalRows": 98,
"processedCount": 98,
"addedCount": 12,
"updatedCount": 86,
"skippedCount": 0,
"failedCount": 0
}
],
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:41.000Z"
},
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-09-30T12:00:41.000Z"
}
}
```
```json 409 theme={null}
{
"success": false,
"code": "sync_disabled",
"error": "This sync is paused. Enable it before running it."
}
```
```json 403 theme={null}
{
"success": false,
"code": "missing_scope",
"error": "Starting automations or sending double opt-in emails from a sync requires the automations:trigger permission."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse sync not found."
}
```
# List Warehouse Sync Runs
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-runs
GET /api/v1/warehouse-syncs/{id}/runs
Recent runs with row counts and import progress
Lists runs from the last 30 days, newest first. Requires `warehouse:read`.
* `rowsUnchanged`: rows skipped because they did not change since they were last sent.
* `rowsSkipped`: rows missing required values, or events for contacts that do not exist yet. They are read again when the row changes, on a full resync, or on every run for syncs without a cursor column.
* `rowsFailed`: rows Sequenzy could not write this time. The cursor stays at the first of them, so the next run reads them again. A row that keeps failing where the previous run stopped is skipped with a note in `errorSamples`; a full resync sends it again.
* `errorSamples`: up to 20 examples with the row number in the query result.
* `imports`: progress of the subscriber imports the run queued. A run finishes when its rows are queued; imports can still be processing.
Sync ID.
Between 1 and 100.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/warehouse-syncs/wsync_abc123/runs?limit=5" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 theme={null}
{
"success": true,
"runs": [
{
"id": "wrun_1",
"syncId": "wsync_abc123",
"status": "succeeded",
"isManual": false,
"isFullResync": false,
"rowsRead": 1204,
"rowsUnchanged": 1100,
"rowsSynced": 98,
"rowsSkipped": 6,
"rowsFailed": 0,
"errorSamples": [
{
"row": 17,
"reason": "Missing email"
}
],
"subscriberImportIds": [
"imp_1"
],
"imports": [
{
"id": "imp_1",
"status": "completed",
"totalRows": 98,
"processedCount": 98,
"addedCount": 12,
"updatedCount": 86,
"skippedCount": 0,
"failedCount": 0
}
],
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:41.000Z"
}
]
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse sync not found."
}
```
# Update Warehouse Sync
Source: https://docs.sequenzy.com/api-reference/warehouse/syncs-update
PATCH /api/v1/warehouse-syncs/{id}
Change a sync, or pause and resume it
Updates a sync. Send only what changes. Requires `warehouse:write` and `subscribers:write` (plus `events:write` for event syncs).
* `query`, `mapping` and `cursorColumn` are checked against the query's columns. A changed query or cursor reads from the beginning again; unchanged rows are still skipped.
* Other changes save without contacting the warehouse.
* Turning on `triggerAutomations` or `double_opt_in` needs `automations:trigger`. On a sync that already has either on, so does changing `query`, `mapping`, `cursorColumn` or `listIds`.
* Saving makes you the user the sync's imports run as.
* Setting `frequency` to `manual` cancels the next scheduled run.
* A run in progress stops at its next page of rows and runs again with the new settings.
Sync ID.
Replaces the mapping. See [Create](/api-reference/warehouse/syncs-create).
`null` removes the cursor.
Lists that contacts the sync creates join. Existing contacts keep theirs.
`false` pauses the sync, `true` resumes it.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/warehouse-syncs/wsync_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "frequency": "daily", "isEnabled": false }'
```
```json 200 theme={null}
{
"success": true,
"sync": {
"id": "wsync_abc123",
"connectionId": "wconn_abc123",
"name": "Customers",
"kind": "subscribers",
"query": "select email, first_name, plan, updated_at from analytics.customers",
"mapping": {
"emailColumn": "EMAIL",
"firstNameColumn": "FIRST_NAME",
"attributes": [
{
"column": "PLAN",
"attribute": "plan"
}
]
},
"cursorColumn": "UPDATED_AT",
"cursorValue": "2026-09-30T11:58:02.000Z",
"listIds": [],
"optInMode": "confirmed",
"triggerAutomations": false,
"frequency": "daily",
"isEnabled": false,
"fullResyncRequested": false,
"status": "idle",
"nextRunAt": "2026-09-30T13:00:00.000Z",
"lastRunAt": "2026-09-30T12:00:00.000Z",
"lastSuccessAt": "2026-09-30T12:00:41.000Z",
"lastError": null,
"lastErrorAt": null,
"consecutiveFailures": 0,
"latestRun": {
"id": "wrun_1",
"syncId": "wsync_abc123",
"status": "succeeded",
"isManual": false,
"isFullResync": false,
"rowsRead": 1204,
"rowsUnchanged": 1100,
"rowsSynced": 98,
"rowsSkipped": 6,
"rowsFailed": 0,
"errorSamples": [
{
"row": 17,
"reason": "Missing email"
}
],
"subscriberImportIds": [
"imp_1"
],
"imports": [
{
"id": "imp_1",
"status": "completed",
"totalRows": 98,
"processedCount": 98,
"addedCount": 12,
"updatedCount": 86,
"skippedCount": 0,
"failedCount": 0
}
],
"error": null,
"startedAt": "2026-09-30T12:00:00.000Z",
"finishedAt": "2026-09-30T12:00:41.000Z"
},
"createdAt": "2026-09-30T10:00:00.000Z",
"updatedAt": "2026-09-30T12:00:41.000Z"
}
}
```
```json 403 theme={null}
{
"success": false,
"code": "missing_scope",
"error": "Starting automations or sending double opt-in emails from a sync requires the automations:trigger permission."
}
```
```json 404 theme={null}
{
"success": false,
"code": "not_found",
"error": "Warehouse sync not found."
}
```
# Create Web Tracking Key
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/create
POST /api/v1/web-tracking-keys
Create a publishable key and get the install snippet for your site
Create a publishable key for the browser tracking SDK and get the `",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/api/comp_123/customer-events"
},
"message": "Web tracking key created. Paste installSnippet into every page, mint an identity token from your backend, then call sequenzy.identify(email, identityToken)."
}
```
```json 400 theme={null}
{
"error": "allowedOrigins contains entries that are not valid origins: *.com. Use a scheme and host such as https://example.com, or https://*.example.com for subdomains."
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
# Delete Web Tracking Key
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/delete
DELETE /api/v1/web-tracking-keys/{id}
Permanently delete a publishable web tracking key
Permanently delete a web tracking key.
Cached authorization expires within one minute after deletion. Requests from any page still running the snippet are rejected after that, so remove the snippet from your site as well. Prefer [revoking](/api-reference/web-tracking-keys/update) with `isActive: false` when you may want the key back.
Requires an API key with the `integrations:manage` scope.
## Request
Web tracking key ID.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/web-tracking-keys/wtk_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"message": "Web tracking key deleted. Cached authorization expires within one minute, so remove the snippet too."
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"error": "Web tracking key not found"
}
```
# Get Web Tracking Key
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/get
GET /api/v1/web-tracking-keys/{id}
Get one web tracking key with its install snippet
Get a single web tracking key, including the paste-ready install snippet and the ingest endpoint.
Use this to hand a developer the exact `",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/api/comp_123/customer-events"
}
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"error": "Web tracking key not found"
}
```
# List Web Tracking Keys
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/list
GET /api/v1/web-tracking-keys
List publishable keys for the browser tracking SDK
List the publishable keys that let your website send on-site events into this workspace.
Each key includes a paste-ready install snippet and the origins it is allowed to run on. A key whose `lastUsedAt` is `null` has not successfully authenticated an event yet. The snippet may be undeployed, the site may not have sent an instrumented event, or the origin allowlist may be rejecting its requests.
Shopify stores do not use these keys. They use the [storefront pixel](/api-reference/integrations/pixel-get).
Requires an API key with the `integrations:manage` scope.
## Request
This endpoint takes no parameters.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/web-tracking-keys" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Response fields
Keys in this workspace, newest first.
The publishable key. It ships in your page source, so it is not secret; it
authorizes storefront events only.
Normalized origins allowed to use this key. Empty means any origin.
True when the allowlist is empty, so any site can send events with this key.
False when the key has been revoked. Revoked keys are rejected at ingest.
When the key last authorized an event, or `null` if it never has. Updated at
most every five minutes.
The exact `",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/api/comp_123/customer-events"
}
]
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
# Mint Web Tracking Identity
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/mint-identity
POST /api/v1/web-tracking-identities
Mint a short-lived proof that lets the browser SDK attach events to one authenticated contact
Call this endpoint from your authenticated backend after you know the current
user's email. It returns a short-lived token bound to the workspace, web
tracking key, normalized email, and expiry.
Never call this endpoint from browser code or expose the secret API key used to
authorize it. A publishable tracking key alone may append anonymous activity,
but cannot attach events to a subscriber or trigger automation.
If the email is not yet a contact in your workspace, minting creates one so the
identified events have somewhere to land - otherwise they would be accepted and
then silently dropped. The contact is created active with no list memberships
and without triggering any automations; enroll it through your usual flows when
you want more than event attribution. If the email cannot be added as a contact
(for example a suppressed or undeliverable address), the request fails with a
400 and no token is minted.
Requires `commerce:write`, `automations:trigger`, and `subscribers:write` (minting can create the contact).
## Request
Identify the key by either `keyId` or `publicKey`. The publishable key is the
value in your snippet and in the dashboard, so it is usually the one your
backend already has.
Publishable key value (`seq_pk_...`) of an active web tracking key. Provide
this or `keyId`.
Internal ID of an active web tracking key. Provide this or `publicKey`.
Email of the user authenticated by your backend. It is normalized to
lowercase.
Token lifetime from 1 minute through 30 days. Defaults to 24 hours. Mint a
fresh token when the user signs in or reaches checkout; request a longer
lifetime only when re-minting is genuinely impossible.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/web-tracking-identities" \
-H "Authorization: Bearer YOUR_SECRET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"publicKey": "seq_pk_your_key",
"email": "buyer@example.com"
}'
```
Pass the returned token to the browser and identify the same email:
```javascript theme={null}
sequenzy.identify("buyer@example.com", response.identityToken);
```
## Response
```json 200 theme={null}
{
"success": true,
"keyId": "wtk_abc123",
"publicKey": "seq_pk_your_key",
"email": "buyer@example.com",
"subscriberId": "sub_abc123",
"identityToken": "seq_wid_...",
"expiresAt": "2026-08-18T12:00:00.000Z"
}
```
```json 400 theme={null}
{
"error": "Provide keyId or publicKey"
}
```
```json 400 (email rejected) theme={null}
{
"error": "This email cannot be added as a contact, so an identity cannot be minted for it"
}
```
```json 404 theme={null}
{
"error": "Active web tracking key not found"
}
```
# Update Web Tracking Key
Source: https://docs.sequenzy.com/api-reference/web-tracking-keys/update
PATCH /api/v1/web-tracking-keys/{id}
Rename a key, replace its allowed origins, or revoke it
Rename a web tracking key, replace its allowed origins, or revoke it.
Requires an API key with the `integrations:manage` scope.
## Request
Web tracking key ID.
New label for the key.
Replacement allowlist. This **replaces** the whole list rather than appending
to it, so include the origins you want to keep. Pass an empty array to make
the key unrestricted.
Set `false` to revoke, `true` to re-enable a revoked key.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/web-tracking-keys/wtk_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"allowedOrigins": ["https://example.com"]}'
```
## Behavior
Revoking takes effect within about a minute and does not rotate the key value, so you can still find and remove the matching snippet from your site. Use [Delete Web Tracking Key](/api-reference/web-tracking-keys/delete) once the snippet is gone.
Fields you do not send are left unchanged. Send at least one of `name`,
`allowedOrigins`, or `isActive`.
## Responses
```json 200 theme={null}
{
"success": true,
"key": {
"id": "wtk_abc123",
"name": "Storefront",
"publicKey": "seq_pk_hR8xQ2mK9vL4nP7wT1yZ6cB3dF5gJ0sA",
"allowedOrigins": ["https://example.com"],
"isActive": true,
"unrestricted": false,
"lastUsedAt": "2026-08-17T09:12:00.000Z",
"createdAt": "2026-08-01T10:00:00.000Z",
"updatedAt": "2026-08-17T10:30:00.000Z",
"installSnippet": "",
"endpoint": "https://api.sequenzy.com/api/webhooks/commerce/api/comp_123/customer-events"
}
}
```
```json 400 theme={null}
{
"error": "allowedOrigins contains entries that are not valid origins: ftp://example.com. Use a scheme and host such as https://example.com, or https://*.example.com for subdomains."
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{
"error": "API key is missing required scope: integrations:manage"
}
```
```json 404 theme={null}
{
"error": "Web tracking key not found"
}
```
# Outbound Webhooks
Source: https://docs.sequenzy.com/api-reference/webhooks/outbound
POST /v1/webhooks
Create and manage outbound webhook endpoints for campaign, email, SMS, subscriber, and sequence lifecycle events.
You can configure an endpoint to receive Sequenzy campaign, email, SMS, subscriber, and sequence lifecycle events as signed JSON POST requests.
## Create
Human-readable webhook name.
Absolute HTTPS endpoint URL.
Event types to receive. Omit this field to receive the default lifecycle
events: sent, delivered, delayed, bounced, complained, email unsubscribed, SMS
lifecycle (sms.sent, sms.delivered, sms.failed, sms.opted\_out), invalid
subscriber, subscriber created, and subscriber unsubscribed. Add opened,
campaign.sent, clicked, replied, subscriber.updated,
subscriber.list\_subscribed, subscriber.list\_unsubscribed, poll.answered,
subscriber\_import.completed, sequence.finished, and sequence.failed explicitly
if you need aggregate campaign completion, engagement, inbound reply, poll
response, profile sync, per-list consent sync, import completion, or sequence
lifecycle events. `campaign.sent` is not in the default set.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/v1/webhooks" \
-H "Authorization: Bearer $SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production webhook",
"url": "https://example.com/sequenzy/webhooks",
"events": ["email.delivered", "email.bounced", "email.opened"]
}'
```
## Manage
```bash theme={null}
# List endpoints
curl "https://api.sequenzy.com/v1/webhooks" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# Update endpoint (disable it but keep it, with its delivery history)
curl -X PATCH "https://api.sequenzy.com/v1/webhooks/webhook_123" \
-H "Authorization: Bearer $SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "disabled"}'
# Permanently delete endpoint and its delivery history
curl -X DELETE "https://api.sequenzy.com/v1/webhooks/webhook_123" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# Add signing secret
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/secrets" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# Remove signing secret
curl -X DELETE "https://api.sequenzy.com/v1/webhooks/webhook_123/secrets/sec_123" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# Send a test event
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/test" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# List latest delivery attempt summary
curl "https://api.sequenzy.com/v1/webhooks/webhook_123/deliveries/del_123/attempts" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
# Replay a delivery
curl -X POST "https://api.sequenzy.com/v1/webhooks/webhook_123/deliveries/del_123/replay" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
```
Updating an endpoint URL, enabling a disabled endpoint, sending a test event, and replaying a delivery reset the endpoint failure state so a recovered receiver can be tested immediately.
`failingSince` is when the current run of failed deliveries started, and it is `null` once a delivery succeeds. If every delivery fails for 7 days, Sequenzy disables the endpoint, skips its pending deliveries, emails the workspace owner, and sets `autoDisabledAt`. Enabling the endpoint or changing its URL clears both fields.
A test event targets the endpoint you name, so it is delivered even when that endpoint subscribes to no event types. The response returns the queued `delivery`, so you can poll that delivery ID for the result instead of waiting for it to show up in the delivery list:
```json theme={null}
{
"success": true,
"eventId": "evt_123",
"queuedDeliveries": 1,
"delivery": {
"id": "del_123",
"eventId": "evt_123",
"eventType": "email.sent",
"status": "pending",
"attempts": 0
}
}
```
## Event Payload
Each webhook delivery sends a signed JSON event to your endpoint. Email events include Sequenzy IDs and, when available, your subscriber external ID or the `subscriberExternalId` stored on a transactional send:
```json theme={null}
{
"id": "evt_123",
"type": "email.sent",
"object": "event",
"metric": "sent",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"email_send_id": "send_123",
"message_id": "ses-message-id",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"recipient": "user@example.com",
"subject": "Welcome",
"email_type": "campaign",
"computed_lists": [
{
"key": "recommendedEvents",
"items": [
{
"id": "evt_123",
"title": "Auckland Theatre"
}
],
"exposures": [
{
"slot": 1,
"id": "evt_123",
"title": "Auckland Theatre"
}
]
}
],
"metadata": {
"source": "campaign"
}
}
}
```
Webhook payloads use one canonical snake\_case field per value. `external_id` is included when the recipient is linked to a subscriber with a customer-owned external ID, or when a single-recipient transactional send included `subscriberExternalId` even if no subscriber exists. `email.sent` events include `computed_lists` when campaign personalization selected per-recipient list items for that email; `items` preserves the original campaign data objects.
`campaign.sent` is one aggregate event when an email or SMS campaign reaches
`sent`, including sends with no recipients and completed email A/B tests. It
does not fire per recipient. The ID is deterministic for the campaign delivery
generation, so a retried or resumed finalization cannot create duplicate
deliveries.
```json theme={null}
{
"id": "evt_campaign_123",
"type": "campaign.sent",
"object": "event",
"metric": "sent",
"created_at": "2026-08-25T12:00:00.000Z",
"data": {
"campaign_id": "camp_123",
"campaign_name": "August launch",
"campaign_type": "email",
"status": "sent",
"sent_at": "2026-08-25T12:00:00.000Z",
"total_sent": 2400,
"total_failed": 3
}
}
```
`campaign_type` is `email`, `sms`, or `push`. For push campaigns, `total_sent` counts contacts whose notification at least one push service accepted and `total_failed` counts contacts whose every device rejected it.
`poll.answered` fires after a Poll or NPS response is stored. For single-answer polls, the nested `poll.value` is the stable scalar value saved to the subscriber attribute, while `poll.answer` is the human-readable label captured for reporting. For multi-select polls, `poll.value` is the complete selected-value array; `poll.values` and `poll.answers` provide parallel value and label arrays, and `poll.allow_multiple` is `true`.
```json theme={null}
{
"id": "evt_poll_123",
"type": "poll.answered",
"object": "event",
"metric": "answered",
"created_at": "2026-05-05T12:05:00.000Z",
"data": {
"email_send_id": "send_123",
"message_id": "ses-message-id",
"campaign_id": "camp_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"recipient": "user@example.com",
"subject": "Tell us what you think",
"email_type": "campaign",
"poll": {
"block_id": "poll_123",
"variant": "options",
"question": "How was your experience?",
"answer": "Great",
"value": "great",
"attribute_key": "experience_rating"
}
}
}
```
A multi-select response uses this `poll` shape:
```json theme={null}
{
"block_id": "poll_interests",
"variant": "options",
"question": "Which topics interest you?",
"answer": "Yoga workshops, Nutrition coaching",
"value": ["yoga", "nutrition"],
"values": ["yoga", "nutrition"],
"answers": ["Yoga workshops", "Nutrition coaching"],
"allow_multiple": true,
"attribute_key": "interests"
}
```
`email.replied` fires when an inbound reply is stored. The payload includes reply, conversation, and original send context plus the reply body text, HTML body, and stripped text. Attachment bodies are not included; only metadata is delivered.
```json theme={null}
{
"id": "evt_reply",
"type": "email.replied",
"object": "event",
"metric": "replied",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"reply_id": "reply_123",
"conversation_id": "conv_123",
"conversation_message_id": "msg_123",
"email_send_id": "send_123",
"message_id": "reply-message-id",
"in_reply_to": "ses-message-id",
"campaign_id": "camp_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"from_email": "user@example.com",
"from_name": "Ada",
"recipient": "user@example.com",
"subject": "Re: Welcome",
"email_type": "campaign",
"body_text": "Thanks for the update.",
"body_html": "
Thanks for the update.
",
"stripped_text": "Thanks for the update.",
"has_attachments": false,
"attachment_count": 0,
"received_at": "2026-05-05T12:00:00.000Z"
}
}
```
Subscriber events include the current subscriber profile when one exists. `subscriber.created` fires once per newly created contact and carries `source` (the origin, using the same vocabulary as the per-list consent events) plus `list_ids` for the lists it joined at creation. It fires for every creation path, including bulk imports and integration syncs, so filter on `source` if you only want organic signups. When double opt-in is on, it fires at confirmation rather than at signup, with `source` set to `double_opt_in` - a contact who never confirms never emits it. `subscriber.invalid` fires when a subscriber add attempt cannot create a sendable subscriber because the attempted email is missing, syntactically invalid, has an invalid/blocked domain, or is already suppressed from previous delivery failures. `subscriber.updated` fires when `email`, `external_id`, `first_name`, `last_name`, `timezone`, `custom_attributes`, or a non-unsubscribe `status` change occurs. Active to unsubscribed changes emit `subscriber.unsubscribed`; they only also emit `subscriber.updated` when another profile field changes in the same update. List and tag changes do not emit `subscriber.updated`.
`subscriber.list_subscribed` and `subscriber.list_unsubscribed` fire only for actual per-list membership transitions. Each payload includes the subscriber ID, email, optional external ID, list ID and name, `action` (`added` or `removed`), and the change `source`. A global opt-out emits `subscriber.unsubscribed` without a per-list event fan-out.
```json theme={null}
{
"id": "evt_list_123",
"type": "subscriber.list_subscribed",
"object": "event",
"metric": "list_subscribed",
"created_at": "2026-07-14T12:00:00.000Z",
"data": {
"subscriber_id": "sub_123",
"external_id": "customer_123",
"email": "user@example.com",
"list_id": "list_product_updates",
"list_name": "Product updates",
"action": "added",
"source": "api"
}
}
```
```json theme={null}
{
"id": "evt_invalid",
"type": "subscriber.invalid",
"object": "event",
"metric": "invalid",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"email": "bad@gmai.com",
"external_id": "customer_123",
"first_name": "Ada",
"custom_attributes": {
"plan": "pro"
},
"reason_code": "invalid_email_domain",
"reason": "Invalid domain for \"bad@gmai.com\": Domain is blacklisted (typosquatting or disposable email)",
"metadata": {
"source": "add_subscriber"
}
}
}
```
```json theme={null}
{
"id": "evt_789",
"type": "subscriber.updated",
"object": "event",
"metric": "updated",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"subscriber_id": "sub_123",
"external_id": "customer_new",
"email": "new@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"timezone": "America/New_York",
"status": "active",
"custom_attributes": {
"plan": "pro"
},
"changed_fields": ["email", "external_id", "timezone", "custom_attributes"],
"previous": {
"email": "old@example.com",
"external_id": "customer_old",
"timezone": "America/Chicago",
"custom_attributes": {
"plan": "starter"
}
}
}
}
```
Sequence lifecycle events include the subscriber email, external ID when available, and the event data recorded for that sequence enrollment:
```json theme={null}
{
"id": "evt_456",
"type": "sequence.finished",
"object": "event",
"metric": "finished",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"sequence_id": "seq_welcome",
"sequence_name": "Welcome sequence",
"automation_id": "seq_welcome",
"automation_name": "Welcome sequence",
"automation_token_id": "token_123",
"token_id": "token_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"email": "user@example.com",
"lifecycle": "finished",
"status": "completed",
"current_node_id": "node_end",
"event": {
"id": "event_123",
"name": "sequence_finished",
"properties": {
"sequence_id": "seq_welcome",
"sequence_name": "Welcome sequence",
"automation_id": "seq_welcome",
"automation_name": "Welcome sequence",
"automation_token_id": "token_123",
"token_id": "token_123",
"lifecycle": "finished",
"status": "completed",
"current_node_id": "node_end"
}
}
}
}
```
## Responses
```json 201 theme={null}
{
"success": true,
"webhook": {
"id": "webhook_123",
"name": "Production webhook",
"url": "https://example.com/sequenzy/webhooks",
"status": "enabled",
"events": ["email.delivered", "email.bounced", "email.opened"],
"consecutiveFailures": 0,
"circuitOpenedAt": null,
"circuitOpenUntil": null,
"failingSince": null,
"autoDisabledAt": null,
"signingSecret": "whsec_...",
"signingSecrets": [
{
"id": "sec_123",
"prefix": "whsec_abcd12",
"createdAt": "2026-05-05T12:00:00.000Z"
}
]
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Unsupported webhook event type: email.unknown"
}
```
```json 401 theme={null}
{
"success": false,
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 404 theme={null}
{
"success": false,
"error": "Webhook not found"
}
```
## Verify Requests
Webhook requests include `X-Sequenzy-Timestamp` and `X-Sequenzy-Signature`. Build the signed payload as `v1:{timestamp}:{raw_request_body}`, compute an HMAC-SHA256 digest with each active webhook signing secret, and compare it to any `v1=` signature value.
If a webhook has multiple active signing secrets, Sequenzy still sends one POST request and includes one `v1=` signature per secret in the same header. Add a new secret, deploy it in your receiver, then remove the old secret.
Return a `2xx` status within 4 seconds. Failed deliveries receive up to 16 attempts with exponential backoff spanning 7 days.
# Configure Domain Tracking
Source: https://docs.sequenzy.com/api-reference/websites/configure
PATCH /api/v1/websites/{domain}
Deprecated: set the company tracking domain from a sending domain
Deprecated. Tracking is company-wide: every sending domain uses one tracking
domain. Use [Set Tracking Domain](../tracking-domain/set) instead, which can
also change it.
This endpoint keeps working for existing integrations. It sets
`.`, such as `links.mail.example.com`, as your company
tracking domain when your company has none. Publish the returned
`website.tracking.cnameRecord`. Sending never waits for it: links use Sequenzy's
shared tracking domain until it verifies.
Sending your company's current tracking hostname again changes nothing. When
your company already uses another tracking domain, the request still succeeds
but changes nothing, and `message` explains why; it never replaces your company
tracking domain. A hostname connected to another company returns 409. This
endpoint requires the `websites:write` scope.
## Request
Configured sending domain.
Tracking subdomain label, for example `links`. Use one DNS label of 1 to 63
letters, numbers or hyphens, without a leading or trailing hyphen. It cannot
be `inbound` or the label that holds the domain's bounce records.
An invalid label returns 400. A missing or non-string `trackingPrefix` returns 422. A tracking provider or other server error returns 500; retrying with the
same value is safe.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/websites/mail.example.com" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"trackingPrefix": "links"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"website": {
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "verified",
"dnsVerified": true,
"readyToSend": true,
"tracking": {
"policy": "legacy",
"required": false,
"hostname": "links.mail.example.com",
"status": "pending",
"ready": false,
"error": null,
"cnameRecord": {
"type": "CNAME",
"name": "links.mail.example.com",
"value": "links1.sequenzydns.com"
}
}
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "This subdomain already holds the domain's bounce records. Choose another tracking subdomain."
}
```
```json 404 theme={null}
{
"success": false,
"code": "SENDING_DOMAIN_NOT_CONFIGURED",
"error": "Sending domain not configured",
"title": "Sending domain not configured",
"docsUrl": "https://docs.sequenzy.com/api-reference/websites/create",
"details": { "domain": "mail.example.com" }
}
```
```json 409 theme={null}
{
"success": false,
"error": "The tracking hostname links.mail.example.com is already in use. Choose another tracking subdomain."
}
```
# Add Sending Domain
Source: https://docs.sequenzy.com/api-reference/websites/create
POST /api/v1/websites
Add a sending domain and get its DNS setup records
Add a sending domain to the authenticated company. Publish the DKIM record,
the bounce records described by `website.bounceRecords`, DMARC when present,
and the optional tracking record before running verification. Newly added
domains always return the unified Sequenzy record set with required DMARC, and
publish their bounce subdomain as one CNAME (`website.bounceRecords.cnameRecord`)
that stands in for the `spfRecord` and `mxRecord` entries, so it also works
where your DNS host blocks custom MX records. Verification checks SPF and MX
through the CNAME. Re-adding a domain that already exists returns its stored
records, which for domains created before the unified rollout can still be
the legacy MAIL FROM and inbound-reply shape.
For a legacy custom reply domain, run
[Verify Sending Domain](/api-reference/websites/verify) after publishing its
inbound MX. If that reply hostname needs separate ownership verification, the
result includes `dnsRecords.inboundVerificationRecord`. Publish its TXT and
verify again. Sending readiness and reply-routing readiness are separate;
use `dnsRecords.inboundRoutingStatus` for replies. Existing reply addresses
remain unchanged.
Tracked links and opens use your company tracking domain, which every sending
domain shares. When your company has none yet, adding a domain creates it on
the domain's root, `links.` by default (`links.example.com` for
`mail.example.com`), and returns its CNAME in `dnsRecords.trackingRecord`.
It is optional: the domain verifies and sends without it, and links use
Sequenzy's shared tracking domain until it verifies. Once your tracking domain
works, later domains reuse it with nothing to add. `website.tracking` reports
its status; manage it with [Set Tracking Domain](../tracking-domain/set).
A sending domain belongs to exactly one company, so confirm the target company
before adding it.
MCP clients should call `add_sending_domain`. The older `add_website` tool remains available as a compatibility alias.
Do not hard-code the example record count or provider hostnames. Treat the
returned `dnsRecords` object as authoritative so clients work with both
unified and legacy domains.
## Request
Domain to add.
Bounce (MAIL FROM) subdomain label. The bounce CNAME is published at
`.`. Use one DNS label of 1 to 63 letters, numbers or
hyphens, without a leading or trailing hyphen; `inbound` is reserved. It applies
only when the domain is created.
Label of the company tracking domain created on the domain's root,
`.`. It is used only when your company has no tracking
domain yet and is ignored otherwise. When the hostname is in use, the label
followed by 2 to 5 is tried (for example `links2`); when none is free, the
domain is added without a tracking domain. Change the tracking domain later with
[Set Tracking Domain](../tracking-domain/set).
Invalid labels, or a tracking label that lands on the bounce hostname, return 400. Re-adding a domain that already exists returns its stored records and
ignores both labels.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/websites" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "mail.example.com"}'
```
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/websites" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "mail.example.com", "mailFromPrefix": "bounce", "trackingPrefix": "click"}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"website": {
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "not_started",
"dnsVerified": false,
"readyToSend": false,
"readiness": {
"status": "setup_required",
"reason": "dns_not_started"
},
"dnsRecords": {
"dkimType": "byodkim",
"byodkimRecord": {
"name": "sequenzy._domainkey.mail.example.com",
"value": "p=PUBLIC_KEY"
},
"byodkimStatus": "pending",
"spfRecord": {
"name": "send.mail.example.com",
"value": "v=spf1 include:spf.sequenzydns.com ~all"
},
"spfStatus": "pending",
"mxRecord": {
"name": "send.mail.example.com",
"value": "bounce.sequenzydns.com",
"priority": 10
},
"mxStatus": "pending",
"dmarcRecord": {
"name": "_dmarc.example.com",
"suggestedValue": "v=DMARC1; p=none;"
},
"dmarcStatus": "pending",
"mailFromDomain": "send.mail.example.com",
"returnPathCnameRecord": {
"name": "send.mail.example.com",
"value": "send.sequenzydns.com"
},
"returnPathCnameStatus": "pending",
"trackingRecord": {
"name": "links.example.com",
"value": "links1.sequenzydns.com"
},
"trackingStatus": "not_started"
},
"dkim": {
"type": "byodkim",
"status": "pending",
"diagnostic": null
},
"spf": {
"status": "pending",
"record": {
"name": "send.mail.example.com",
"value": "v=spf1 include:spf.sequenzydns.com ~all"
},
"diagnostic": null
},
"mailFrom": {
"status": "pending",
"mxRecord": {
"name": "send.mail.example.com",
"value": "bounce.sequenzydns.com",
"priority": 10
},
"diagnostic": null
},
"bounceRecords": {
"style": "cname",
"cnameRecord": {
"name": "send.mail.example.com",
"value": "send.sequenzydns.com"
},
"status": "pending",
"diagnostic": null
},
"tracking": {
"policy": "legacy",
"required": false,
"hostname": "links.example.com",
"status": "not_started",
"ready": false,
"error": null,
"cnameRecord": {
"type": "CNAME",
"name": "links.example.com",
"value": "links1.sequenzydns.com"
}
},
"lastVerifiedAt": null,
"nextVerificationAt": null,
"createdAt": "2026-07-23T12:00:00.000Z",
"message": "Domain added. Configure DNS records; sending becomes available once DNS is verified and activation finishes."
}
}
```
```json 200 theme={null}
{
"success": true,
"website": {
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "not_started",
"dnsRecords": {
"dkimType": "byodkim",
"byodkimRecord": {
"name": "sequenzy._domainkey.mail.example.com",
"value": "p=PUBLIC_KEY"
},
"byodkimStatus": "pending",
"spfRecord": {
"name": "send.mail.example.com",
"value": "v=spf1 include:amazonses.com ~all"
},
"spfStatus": "pending",
"mxRecord": {
"name": "send.mail.example.com",
"value": "feedback-smtp.us-east-1.amazonses.com",
"priority": 10
},
"mxStatus": "pending",
"inboundMxRecord": {
"name": "inbound.mail.example.com",
"value": "inbound-smtp.us-east-1.amazonaws.com",
"priority": 10
},
"inboundMxStatus": "pending",
"mailFromDomain": "send.mail.example.com"
},
"dkim": {
"type": "byodkim",
"status": "pending",
"diagnostic": null
},
"spf": {
"status": "pending",
"record": {
"name": "send.mail.example.com",
"value": "v=spf1 include:amazonses.com ~all"
},
"diagnostic": null
},
"mailFrom": {
"status": "pending",
"domain": "send.mail.example.com",
"mxRecord": {
"name": "send.mail.example.com",
"value": "feedback-smtp.us-east-1.amazonses.com",
"priority": 10
},
"diagnostic": null
},
"bounceRecords": {
"style": "mx_txt",
"cnameRecord": null,
"status": "pending",
"diagnostic": null
},
"appleEmailSources": ["send.mail.example.com", "example.com"],
"tracking": {
"policy": "legacy",
"required": false,
"hostname": null,
"status": null,
"ready": false,
"error": null,
"cnameRecord": null
},
"lastVerifiedAt": null,
"nextVerificationAt": null,
"createdAt": "2026-07-13T12:00:00.000Z",
"message": "Domain already exists"
}
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid domain"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Bounce and tracking subdomains must be different."
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 409 theme={null}
{
"success": false,
"code": "DOMAIN_ALREADY_CONFIGURED",
"error": "Domain already configured",
"title": "Domain already configured",
"description": "The domain \"mail.example.com\" is already configured as a sending domain in Sequenzy. Sending domains are globally unique, so the same domain cannot be added to another company.",
"resolution": "Call list_websites for the current company. If the domain is listed, use check_website for its DNS status. If it is not listed, it belongs to another company; the API, CLI and MCP cannot move or remove it. Ask the user whether to use a different sending domain, or have an owner remove it from the other company in the Sequenzy dashboard (Settings > Domain) and then add it here. Removal deletes the domain's senders and requires publishing a new DKIM record and verifying DNS again.",
"docsUrl": "https://docs.sequenzy.com/api-reference/websites/create",
"details": {
"domain": "mail.example.com"
}
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to create domain"
}
```
# Get Website
Source: https://docs.sequenzy.com/api-reference/websites/get
GET /api/v1/websites/{domain}
Get sending-domain DNS details
Get stored DNS verification, sending readiness,
expected DNS records, and normalized SPF, DKIM, and MAIL FROM diagnostics for a
sending domain. A domain can have `dnsVerified: true` while `readyToSend` is
false while the domain is still activating. To run a fresh check, use
[Verify Sending Domain](/api-reference/websites/verify).
For custom replies, `website.dnsRecords.inboundRoutingStatus` reports
`pending`, `active`, or `failed` readiness from the latest stored checks.
Legacy SES routes remain pending until receiving ownership has been checked;
this display does not disable an existing route. Retained MTA reply routes
keep their existing status. `inboundRoutingError` provides recovery guidance,
or null when no error is stored. An optional `inboundVerificationRecord` contains the
public TXT `name` and `value` to publish for an uncovered legacy reply domain.
`inboundVerificationStatus` reports domain ownership separately from routing
and sending readiness. This GET reads stored results; it does not prepare a
missing token or perform a DNS check. Use the verify endpoint to prepare or
retry the record. These generated fields cannot be edited or cleared by API.
`website.tracking` reports link tracking: the company tracking domain, which
every sending domain uses. `hostname` is null when links use Sequenzy's shared
tracking domain, and `ready` is true once it verifies. It never affects the
domain's status or sending. `policy` is always `legacy` and `required` always
`false`; both are deprecated. Manage it with
[Set Tracking Domain](../tracking-domain/set).
## Request
Sending domain.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/websites/mail.example.com" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"website": {
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "verified",
"dnsVerified": true,
"readyToSend": false,
"readiness": {
"status": "activating",
"reason": "activation_pending"
},
"dnsRecords": {},
"dkim": { "type": "byodkim", "status": "pending", "diagnostic": null },
"spf": { "status": "verified", "record": {}, "diagnostic": null },
"mailFrom": { "status": "verified", "domain": "send.mail.example.com", "mxRecord": {}, "diagnostic": null },
"appleEmailSources": ["send.mail.example.com", "example.com"],
"tracking": { "policy": "legacy", "required": false, "hostname": "links.example.com", "status": "verified", "ready": true, "error": null, "cnameRecord": { "type": "CNAME", "name": "links.example.com", "value": "links1.sequenzydns.com" } },
"lastVerifiedAt": "2026-05-01T10:30:00Z",
"nextVerificationAt": "2026-05-01T11:30:00Z",
"createdAt": "2026-05-01T10:30:00Z"
}
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
```json 404 theme={null}
{
"success": false,
"code": "SENDING_DOMAIN_NOT_CONFIGURED",
"error": "Sending domain not configured",
"title": "Sending domain not configured",
"description": "The domain \"mail.example.com\" has not been added as a sending domain for this company, so it cannot be checked or verified yet.",
"resolution": "Call add_sending_domain with domain \"mail.example.com\" first, publish the returned SPF, DKIM, MAIL FROM, and inbound MX DNS records, then call verify_sending_domain after DNS changes propagate.",
"docsUrl": "https://docs.sequenzy.com/api-reference/websites/create",
"details": {
"domain": "mail.example.com"
}
}
```
# List Websites
Source: https://docs.sequenzy.com/api-reference/websites/list
GET /api/v1/websites
List sending domains for the current company
List sending domains with separate DNS verification, sending readiness, and
SPF, DKIM, and MAIL FROM status. `readyToSend` requires verified DNS plus
finished activation for sending.
## Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/websites" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"websites": [
{
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "verified",
"dnsVerified": true,
"readyToSend": true,
"readiness": {
"status": "ready",
"reason": null
},
"dkim": { "type": "byodkim", "status": "verified", "diagnostic": null },
"spf": { "status": "verified", "record": {}, "diagnostic": null },
"mailFrom": { "status": "verified", "domain": "send.mail.example.com", "mxRecord": {}, "diagnostic": null },
"appleEmailSources": ["send.mail.example.com", "example.com"],
"tracking": { "policy": "legacy", "required": false, "hostname": "links.example.com", "status": "verified", "ready": true, "error": null, "cnameRecord": { "type": "CNAME", "name": "links.example.com", "value": "links1.sequenzydns.com" } },
"createdAt": "2026-05-01T10:30:00Z"
}
]
}
```
```json 401 theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
# Verify Sending Domain
Source: https://docs.sequenzy.com/api-reference/websites/verify
POST /api/v1/websites/{domain}/verify
Run a fresh SPF, DKIM, and MAIL FROM verification
Run the sending-domain DNS verification pipeline immediately and return DNS
status separately from sending readiness.
`verified` is the fresh DNS verdict. The company tracking domain is never part
of it; this check also rechecks the tracking domain without letting it affect
the domain. `readyToSend` additionally requires the domain to finish
activating for sending. `message` reports both: when DNS is
verified but activation is still running, it says so instead of claiming the
domain can send. This endpoint uses the same verification process as the
dashboard and updates the stored status.
## Custom reply DNS recovery
Reply routing is separate from sending readiness. Once a legacy reply domain's
MX verifies, this endpoint prepares any additional ownership TXT record in
`website.dnsRecords.inboundVerificationRecord` (`name` and `value`). The value
is a public DNS token, not an API credential. Publish it at the returned fully
qualified name and call this endpoint again after DNS propagation. A pending
verification returns HTTP 200 with `inboundRoutingStatus: "failed"` and a
retry instruction in `inboundRoutingError`; it does not make a verified sending
domain unable to send.
`inboundVerificationStatus` reports ownership only. Use
`inboundRoutingStatus: "active"` to confirm the stored routing checks passed.
Missing MX leaves routing pending. Provider lookup/preparation errors can be
retried with this same endpoint. An inconclusive ownership lookup preserves an
existing active route and records an error rather than discarding accepted
replies. Previously confirmed ownership remains verified; an unchecked legacy
SES route still displays pending until its first successful ownership check.
`discarded: true` means a concurrent update superseded this check;
read the current website or retry before acting on stale results.
Repeated checks reuse a pending token. Expired verification is restarted and
the current returned token is authoritative. Already-sent reply addresses and
their MX remain unchanged. A required TXT stays available after verification;
an unnecessary pending TXT instruction disappears when a parent verifies.
These generated fields are read-only: no request parameter sets, clears, or
replaces them. A missing record alone does not prove routing is ready; its
preparation may not have run or may have failed.
CLI: `sequenzy websites verify mail.example.com --company comp_123`.
MCP: `verify_sending_domain`. Both return the same records and retry status.
## Request
Configured sending domain.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/websites/mail.example.com/verify" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"verified": true,
"readyToSend": false,
"message": "Domain DNS records verified. Sending is not available yet: this domain is still being activated for sending. No further DNS changes are needed - sending unlocks automatically once activation finishes.",
"discarded": false,
"website": {
"id": "domain_abc123",
"domain": "mail.example.com",
"status": "verified",
"dnsVerified": true,
"readyToSend": false,
"readiness": {
"status": "activating",
"reason": "activation_pending"
},
"dkim": { "type": "byodkim", "status": "verified", "diagnostic": null },
"spf": { "status": "verified", "record": {}, "diagnostic": null },
"mailFrom": { "status": "verified", "domain": "send.mail.example.com", "mxRecord": {}, "diagnostic": null },
"appleEmailSources": ["send.mail.example.com", "example.com"],
"tracking": { "policy": "legacy", "required": false, "hostname": "links.example.com", "status": "verified", "ready": true, "error": null, "cnameRecord": { "type": "CNAME", "name": "links.example.com", "value": "links1.sequenzydns.com" } }
}
}
```
```json 404 theme={null}
{
"success": false,
"code": "SENDING_DOMAIN_NOT_CONFIGURED",
"error": "Sending domain not configured",
"title": "Sending domain not configured",
"description": "The domain \"mail.example.com\" has not been added as a sending domain for this company, so it cannot be checked or verified yet.",
"resolution": "Call add_sending_domain with domain \"mail.example.com\" first, publish the DNS records it returns (DKIM, the bounce records described by website.bounceRecords, and DMARC when present), then call verify_sending_domain after DNS changes propagate.",
"docsUrl": "https://docs.sequenzy.com/api-reference/websites/create",
"details": {
"domain": "mail.example.com"
}
}
```
# Create Popup
Source: https://docs.sequenzy.com/api-reference/widgets/create-popup
POST /api/v1/popups
Create an on-site signup popup and get its embed script
Create a saved popup and get the one-line script tag that deploys it. The popup
is published by default, so the script is live as soon as it is added to the
site. Trigger, targeting, audience, and duplicate handling stay server-side, so
the deployed script carries no API key.
For a Webflow-connected workspace, the popup is saved but `embed` contains
`disabled: true`, a reason, and the native Webflow settings path instead of
custom code. Marketplace-connected sites use native Webflow forms only.
Omit `listIds` to capture into every list, matching the dashboard default.
## Request
Internal popup name.
Starting design: `newsletter-modal`, `discount-offer`, `countdown-launch`,
`minimal-slide-in`, `exit-lead-magnet`, `live-demo`, `launch-modal`,
`paper-digest`, `stark-takeover`, `top-bar`, `announcement-bar`, or
`fullscreen-welcome`. Defaults to `newsletter-modal`.
`draft` or `published`. Defaults to `published`.
Lists every signup joins. Omit or pass an empty array to capture into every
list.
Existing tag IDs to apply.
`skip`, `merge`, or `overwrite`. Defaults to `skip`.
What happens when a contact who unsubscribed from all email submits this popup
again. `reactivate` resubscribes them and restores the popup's lists.
`double_opt_in` sends your double opt-in confirmation email first and
resubscribes them once they confirm. Turning on `double_opt_in` needs a double
opt-in confirmation email and a sender profile; otherwise the request returns
`400`. If the email or sender is removed later, returning unsubscribed
contacts stay unsubscribed until you fix it. When workspace double opt-in is
on, contacts always confirm first. `duplicateStrategy` never changes
subscription status.
Text for the popup's first heading block.
Text for the popup's first paragraph block.
Submit button label.
Confirmation shown when `redirectUrl` is omitted.
Optional HTTP or HTTPS success redirect.
`modal`, `slide-in`, `floating-bar`, or `fullscreen`.
`center`, `left`, `right`, `top`, or `bottom`.
When the popup opens: `{ "type": "delay" | "scroll" | "exit-intent" | "click" | "manual", "delaySeconds": 0-3600, "scrollPercent": 1-100, "clickSelector":
"#join" }`. A `click` trigger requires a `clickSelector`.
Where it may show: `{ "domains": [], "paths": [], "excludedPaths": [],
"device": "all" | "desktop" | "mobile" }`. Empty arrays mean no restriction.
A path matches that page and everything under it (`/blog` covers
`/blog/post`), and `/` matches every page. End a path with `$` to match only
that exact page (`/$` is the home page). `*` matches any characters, and a
trailing slash is ignored. Full URLs, with or without `https://`, are
reduced to their path (query strings and fragments are ignored), and a bare
site URL such as `https://example.com/` matches only the home page.
Optional run window: `{ "startsAt": "2026-09-01T00:00:00Z", "endsAt":
"2026-09-08T00:00:00Z" }`. `endsAt` must be later than `startsAt`.
How often one visitor sees it: `{ "maxDisplays": 1-100, "windowDays": 1-365 }`.
Media panel and urgency treatment: `{ "style": "none" | "accent" | "header" |
"rail" | "image" | "countdown", "placement": "top" | "left" | "center" |
"right", "imageUrl": "https://...", "imageAlt": "...", "countdownMinutes":
1-10080 }`. The `image` style requires an https `imageUrl` - the image panel is
painted as a background, so a missing or blocked image would leave an empty
column beside your content.
Visual theme overrides: `accentColor`, `backgroundColor`, `textColor`,
`mutedTextColor`, `cardColor`, `borderColor` (all `#rrggbb`), `borderRadius`
(0-32), `headingFontFamily`, `bodyFontFamily`, `density`.
Complete replacement for the popup's content blocks. The popup must keep
exactly one required email field and one submit button. See [Content
blocks](/api-reference/widgets/update-popup#content-blocks) for each kind's
properties.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/popups" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome offer",
"template": "discount-offer",
"listIds": ["list_123"],
"headline": "Take 10% off your first order",
"trigger": { "type": "exit-intent" }
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"popup": {
"id": "pop_123",
"name": "Welcome offer",
"status": "published",
"viewCount": 0,
"conversionCount": 0,
"publishedAt": "2026-08-10T09:00:00.000Z"
},
"embed": {
"scriptUrl": "https://sequenzy.com/embed/popups/pop_123?v=capture-runtime-20260716-14",
"javascript": "",
"supportedPlatforms": ["HTML", "React", "Next.js", "WordPress", "Shopify"]
},
"message": "Popup created and published. Add the embed script to the site and the popup starts showing under its trigger and targeting rules."
}
```
```json 400 theme={null}
{ "error": "Every listId must belong to the selected company." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:write" }
```
```json 500 theme={null}
{ "error": "Failed to create popup." }
```
# Create Saved Form
Source: https://docs.sequenzy.com/api-reference/widgets/create-saved-form
POST /api/v1/forms
Create a list-scoped public signup form
Create and publish a saved form. The returned opaque `formId` is a client-safe
public capability; the selected lists, tags, duplicate behavior, and success
action remain server-side.
For a Webflow-connected workspace, the form is saved but `embed` contains
`disabled: true`, a reason, and the native Webflow settings path instead of
custom code. Use explicitly selected native Webflow forms.
## Request
Internal form name.
One or more list IDs that every submission joins.
Existing tag IDs to apply.
`skip`, `merge`, or `overwrite`. Defaults to `skip`.
What happens when a contact who unsubscribed from all email submits this form
again. `reactivate` resubscribes them and restores the form's lists.
`double_opt_in` sends your double opt-in confirmation email first and
resubscribes them once they confirm. Turning on `double_opt_in` needs a double
opt-in confirmation email and a sender profile; otherwise the request returns
`400`. If the email or sender is removed later, returning unsubscribed
contacts stay unsubscribed until you fix it. When workspace double opt-in is
on, contacts always confirm first. `duplicateStrategy` never changes
subscription status.
Heading copy: 1-180 visible characters, with up to 1,400 raw characters when
using inline HTML.
Description copy: up to 700 visible characters, with up to 4,000 raw
characters when using inline HTML.
Submit button label, up to 80 characters.
Include a first-name field. Omit to use the default form settings.
Include a last-name field. Omit to use the default form settings.
Visual overrides: `accentColor`, `backgroundColor`, `textColor`,
`mutedTextColor`, `cardColor`, `borderColor` (all `#rrggbb`), `borderRadius`
(0-32), `headingFontFamily`, `bodyFontFamily`, and `density` (`compact`,
`balanced`, or `spacious`). Omitted properties use the default theme.
Confirmation shown when `redirectUrl` is omitted. Up to 240 visible
characters, or 1,700 raw characters including inline HTML.
Optional HTTP or HTTPS success redirect.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/forms" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Blog newsletter",
"listIds": ["list_123"],
"tagIds": ["tag_123"],
"successMessage": "You are subscribed."
}'
```
## Responses
```json 200 theme={null}
{
"success": true,
"form": {
"id": "form_123",
"name": "Blog newsletter",
"status": "published",
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123"
},
"embed": {
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123",
"scriptUrl": "https://api.sequenzy.com/api/v1/forms/form_123/embed.js"
}
}
```
```json 400 theme={null}
{ "error": "Every listId must belong to the selected company." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:write" }
```
```json 500 theme={null}
{ "error": "Failed to create form." }
```
# Delete Popup
Source: https://docs.sequenzy.com/api-reference/widgets/delete-popup
DELETE /api/v1/popups/{popupId}
Permanently delete a saved popup
Permanently delete a saved popup along with its view and conversion counts.
Subscribers it already captured are not affected.
To stop a popup from showing while keeping its stats, set its status to `draft`
with [Update Popup](/api-reference/widgets/update-popup) instead.
```bash theme={null}
curl -X DELETE "https://api.sequenzy.com/api/v1/popups/pop_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"popupId": "pop_123",
"message": "Popup deleted. Remove its embed script from the site - the script now does nothing."
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:delete" }
```
```json 404 theme={null}
{ "error": "Popup not found" }
```
# Duplicate Popup
Source: https://docs.sequenzy.com/api-reference/widgets/duplicate-popup
POST /api/v1/popups/{popupId}/duplicate
Copy a popup into a new draft
Copy a saved popup into a new draft with its own view and conversion counts.
The original keeps its status and stats, so a live popup carries on showing
while you edit the copy.
Use this to test a new offer or headline against a popup that already performs.
## Request
Name for the copy. Defaults to the original name with ` (copy)` appended.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/popups/pop_123/duplicate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Welcome offer v2" }'
```
Publish the copy when it is ready with
[Update Popup](/api-reference/widgets/update-popup).
## Responses
```json 200 theme={null}
{
"success": true,
"popup": {
"id": "pop_456",
"name": "Welcome offer v2",
"status": "draft",
"viewCount": 0,
"conversionCount": 0,
"stats": {
"views": 0,
"starts": 0,
"conversions": 0,
"startRate": null,
"conversionRate": null,
"completionRate": null
}
},
"message": "Popup duplicated as a draft with its own stats. Set status to published when the copy is ready to show."
}
```
```json 400 theme={null}
{ "error": "Popup name is required." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:write" }
```
```json 404 theme={null}
{ "error": "Popup not found" }
```
# Get Popup
Source: https://docs.sequenzy.com/api-reference/widgets/get-popup
GET /api/v1/popups/{popupId}
Read one popup with its complete content
Return one saved popup with its complete content blocks, trigger, targeting,
schedule, frequency, and theme. Read this before replacing `blocks` so the
replacement array stays complete.
The `embed` object is present only when the popup is published.
For a Webflow-connected workspace, a published popup's `embed` object contains
`disabled: true` and the native Webflow settings path instead of custom code.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/popups/pop_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"popup": {
"id": "pop_123",
"name": "Welcome offer",
"status": "published",
"viewCount": 1840,
"conversionCount": 96,
"content": {
"version": 1,
"surface": "popup",
"template": "discount",
"presentation": "modal",
"placement": "center",
"trigger": { "type": "exit-intent", "delaySeconds": 5 },
"targeting": { "domains": [], "paths": [], "device": "all" },
"frequency": { "maxDisplays": 3, "windowDays": 7 },
"blocks": []
}
},
"embed": {
"scriptUrl": "https://sequenzy.com/embed/popups/pop_123?v=capture-runtime-20260716-14"
}
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:read" }
```
```json 404 theme={null}
{ "error": "Popup not found" }
```
# Get Popup Embed
Source: https://docs.sequenzy.com/api-reference/widgets/get-popup-embed
GET /api/v1/popups/embed/{popupId}
Get a published popup's deployment snippets
Return a published popup's script URL plus ready-to-paste snippets for plain
HTML, React and Next.js, WordPress, and Shopify. The snippets carry no API key -
trigger, targeting, and audience rules stay server-side.
The popup must be published. Publish it with
[Update Popup](/api-reference/widgets/update-popup).
For a Webflow-connected workspace, this returns `embed.disabled: true`, a
reason, and the native Webflow settings path instead of custom code.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/popups/embed/pop_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"popup": { "id": "pop_123", "name": "Welcome offer", "status": "published" },
"embed": {
"scriptUrl": "https://sequenzy.com/embed/popups/pop_123?v=capture-runtime-20260716-14",
"javascript": "",
"supportedPlatforms": ["HTML", "React", "Next.js", "WordPress", "Shopify"]
}
}
```
```json 400 theme={null}
{ "error": "Publish the popup before requesting public embed code." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:read" }
```
```json 404 theme={null}
{ "error": "Popup not found" }
```
# Get Saved Form Embed
Source: https://docs.sequenzy.com/api-reference/widgets/get-saved-form-embed
GET /api/v1/forms/embed/{formId}
Get client-safe embed recipes for a published saved form
Return the public action URL, hosted JavaScript, minimal native form, and fetch
enhancement for a saved form owned by your selected workspace.
For a Webflow-connected workspace, this returns `embed.disabled: true`, a
reason, and the native Webflow settings path instead of custom code.
Saved form ID.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/forms/embed/form_123" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"form": {
"id": "form_123",
"name": "Blog newsletter",
"status": "published"
},
"embed": {
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123",
"scriptUrl": "https://api.sequenzy.com/api/v1/forms/form_123/embed.js",
"javascript": "",
"nativeForm": "",
"fetch": "const form = document.querySelector(\"#sequenzy-signup\"); ...",
"supportedPlatforms": ["Astro", "Hugo", "Jekyll", "Cloudflare Pages", "Netlify", "GitHub Pages"]
}
}
```
```json 400 theme={null}
{ "error": "Publish the form before requesting public embed code." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:read" }
```
```json 404 theme={null}
{ "error": "Form not found" }
```
# List Capture Submissions
Source: https://docs.sequenzy.com/api-reference/widgets/list-capture-submissions
GET /api/v1/submissions/{sourceType}/{sourceId}
Read saved submitted answers and payloads for forms, popups and landing pages
Read accepted submissions for a saved form, popup or landing page. Set
`sourceType` to `form`, `popup` or `landing_page`, and `sourceId` to its ID. Your
key needs `subscribers:read` plus `widgets:read` for forms/popups or
`landing_pages:read` for landing pages. Existing management routes keep their
permissions. `/api/v1/forms/{formId}/submissions` is a form-only alias.
```bash theme={null}
curl 'https://api.sequenzy.com/api/v1/submissions/form/form_123?field=session&value=morning' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
## Query and pagination
`limit` is an integer from 1 to 100, default 50. Results are newest first. Pass
`nextCursor` as `cursor` with the same filters to continue; `nextCursor: null`
means there are no more results. Newer submissions arriving during pagination
are excluded from later pages. Start again without a cursor to see them.
Supply both `field` and `value` to filter a custom answer. `field` is the exact
stored attribute name, including literal dots. `value` matches an exact scalar
string representation or a selected string in a checkbox array. Numbers and
booleans use values such as `2` and `false`. An empty value matches a stored empty
string. Omit both to clear the filter. Null query values are not supported.
`format=json` is the default. `format=csv` adds a `csv` string for the returned
page while retaining the JSON response and `nextCursor`. CSV includes identity
fields and the union of custom field names in that page, JSON-encodes complex
answers and neutralizes spreadsheet formulas. Different pages can have different
CSV columns. For one combined file, use the dashboard export or
`sequenzy submissions form form_123 --csv`.
## Saved payload
Each `submissions` item includes `id`, `companyId`, `sourceType`, `sourceId`, `subscriberId`,
`createdAt` (UTC ISO timestamp), `payload`, and `fieldLabels`. `payload` contains
`email`, optional `firstName`, `lastName`, and `phone`, plus `customAttributes`.
`formId`, `popupId`, and `landingPageId` are nullable source references; exactly
one is set. `fieldLabels` maps custom field names to their labels at submission time.
These are accepted, normalized answers, not the raw HTTP request. Headers,
authentication tokens, honeypot fields, unknown custom fields and audience
configuration are excluded. Server-owned hidden values use their configured
value. Records remain unchanged when a subscriber or source is edited, including
when the source's duplicate policy skips updating an existing contact.
History starts when submission tracking is deployed. Explicitly recovered historical records may also appear, and the form's lifetime `submissionCount` can exceed the
number of retained records. Forms, popups and landing pages are covered; generic unsaved forms do not create
these records. A submission is not proof of
confirmed double opt-in. Deleting the subscriber, source or company removes its
records. There is no update or clear operation for individual snapshots.
## Responses and retries
* `200`: `{ success: true, source: { id, name, type }, submissions: [...], nextCursor }`;
includes `csv` only when requested. An empty result has `submissions: []`.
* `400`: invalid cursor, format, page size, or incomplete field/value pair.
* `401`: missing or invalid authentication.
* `403`: missing required scope or workspace access.
* `404`: source missing or outside your workspace.
* `500`: temporary database failure. Reads can be retried without side effects.
Submission capture commits the snapshot and source counter together after subscriber
and tag processing. If capture fails, the public submission fails; retrying can
repeat that earlier processing, including automation or confirmation-email side
effects under existing behavior. Repeated accepted POSTs create separate records.
Form and popup counts increase for every accepted submission; landing-page
conversion counts still increase only for newly created subscribers. This
endpoint does not provide POST idempotency.
The submission-payload tool is available on standard MCP. The OpenAI-reviewed
MCP profile excludes it under its existing raw-payload restrictions; use the
dashboard, API or CLI for this workflow on that profile.
## Reconstructed history
A record with `payload.recovery` was recovered from historical evidence. Its
`sourceAttribution` is `inferred` and `timestampMeaning` is `event_time`: `createdAt`
is the evidence event time, not a verified submission timestamp. `evidenceType`
is `contact.added` or `subscriber.updated`; `evidenceId` identifies the source
record and `recoveredAt` is the recovery execution time. This optional object is
absent for ordinary captured submissions. Missing historical answers are not invented.
The dashboard marks these records **Reconstructed**. CLI human output labels the
event time; JSON API, CLI and standard MCP preserve the recovery object. CSV
adds `record_origin`, `timestamp_meaning` and `recovered_at` columns when its rows
include recovered records. Ordinary rows in such exports use `submission` and
`submission_time`; recovered rows use `reconstructed` and `event_time`.
Without recovered records, CSV columns remain unchanged. In paginated CSV API/MCP
responses, the extra columns depend on the current page. CLI and dashboard CSV
exports collect all matching pages before building one consistent header.
Recovery is an operator-reviewed repair, not a public import endpoint. It does
not update profiles, increment source counters or trigger notifications or automations.
# List Popups
Source: https://docs.sequenzy.com/api-reference/widgets/list-popups
GET /api/v1/popups
List saved on-site signup popups
List the workspace's saved popups with their status, view and conversion
counts, and engagement rates. Popups are the on-site overlay capture surface;
see [List Saved Forms](/api-reference/widgets/list-saved-forms) for inline
embedded forms.
Content blocks are omitted by default, because each popup adds roughly 1.8k
characters. Read one popup's full content with
[Get Popup](/api-reference/widgets/get-popup), or pass `includeContent=true`
when you genuinely need every popup's blocks at once.
## Request
Set to `true` to include every popup's full content blocks.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/popups" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Responses
```json 200 theme={null}
{
"success": true,
"companyId": "comp_123",
"popups": [
{
"id": "pop_123",
"name": "Welcome offer",
"status": "published",
"viewCount": 1840,
"startCount": 310,
"conversionCount": 96,
"stats": {
"views": 1840,
"starts": 310,
"conversions": 96,
"startRate": 0.1685,
"conversionRate": 0.0522,
"completionRate": 0.3097
},
"publishedAt": "2026-08-01T09:00:00.000Z",
"updatedAt": "2026-08-04T11:20:00.000Z",
"url": "https://sequenzy.com/dashboard/company/comp_123/popups/pop_123"
}
],
"note": "Content blocks are omitted. Pass includeContent=true, or read one popup with GET /popups/{popupId}.",
"url": "https://sequenzy.com/dashboard/company/comp_123/popups"
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:read" }
```
# List Saved Forms
Source: https://docs.sequenzy.com/api-reference/widgets/list-saved-forms
GET /api/v1/forms
List saved signup forms and their public action URLs
List the saved forms that belong to your selected workspace. Each result
includes its server-managed audience settings and client-safe public action.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/forms" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Form data
Each form includes:
* `settings`: audience targeting, duplicate handling and success behavior.
* `content`: the complete builder document with `version: 1`, `surface: "form"`,
`template`, `theme`, `settings` and `blocks`. Read `content.blocks` before
[replacing blocks](/api-reference/widgets/update-saved-form#content-blocks).
* `submissionCount`: recorded submissions.
* `publishedAt`: nullable publication timestamp. Unpublishing can preserve it;
use `status` to determine whether the form is currently published.
* `createdAt` and `updatedAt`: ISO 8601 timestamps.
* `actionUrl`: public submission endpoint.
* `url`: dashboard link to edit this form.
The top-level `url` opens your forms dashboard. Create and update responses
return the same form shape. Create returns `embed`; update includes it only
when the form is published. `embed` contains `actionUrl`, `scriptUrl`,
`javascript`, `nativeForm`, `fetch` and `supportedPlatforms`.
## Responses
```json 200 theme={null}
{
"success": true,
"companyId": "company_123",
"forms": [
{
"id": "form_123",
"name": "Blog newsletter",
"status": "published",
"settings": {
"duplicateStrategy": "skip",
"resubscribeBehavior": "reactivate",
"listMode": "specific",
"listIds": [
"list_123"
],
"tagIds": [],
"templateId": "editorial-split",
"themeMode": "design-system",
"formStyle": "button-below",
"buttonText": "Subscribe",
"buttonColor": null,
"placeholderEmail": "you@example.com",
"eyebrowText": "",
"headline": "",
"description": "",
"imageUrl": null,
"imageAlt": "",
"visualPlacement": "none",
"overlayColor": null,
"overlayOpacity": 0.35,
"fontFamily": null,
"fontSize": null,
"fontColor": null,
"borderRadius": null,
"afterSubmission": "message",
"redirectUrl": "",
"successMessage": "Thanks for subscribing!",
"successFontColor": null,
"successFontSize": "14",
"errorMessage": "Something went wrong. Please try again.",
"showFirstName": false,
"showLastName": false,
"placeholderFirstName": "First name",
"placeholderLastName": "Last name",
"firstNameRequired": false,
"lastNameRequired": false,
"fieldOrder": [],
"customFields": []
},
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123",
"content": {
"version": 1,
"surface": "form",
"template": "editorial-split",
"theme": {
"accentColor": "#0f766e",
"backgroundColor": "#ffffff",
"textColor": "#111827",
"mutedTextColor": "#6b7280",
"cardColor": "#ffffff",
"borderColor": "#e5e7eb",
"borderRadius": 8,
"headingFontFamily": "",
"bodyFontFamily": "",
"density": "balanced"
},
"settings": {
"duplicateStrategy": "skip",
"resubscribeBehavior": "reactivate",
"listMode": "specific",
"listIds": [
"list_123"
],
"tagIds": [],
"afterSubmission": "message",
"redirectUrl": ""
},
"blocks": [
{
"id": "email",
"kind": "form-field",
"fieldType": "email",
"label": "",
"showLabel": true,
"name": "email",
"placeholder": "",
"required": true,
"defaultValue": "",
"options": [],
"mapsTo": "email",
"consentText": ""
},
{
"id": "submit",
"kind": "submit-button",
"text": "Subscribe"
}
]
},
"submissionCount": 42,
"publishedAt": "2026-09-01T10:00:00Z",
"createdAt": "2026-09-01T10:00:00Z",
"updatedAt": "2026-09-01T10:00:00Z",
"url": "https://sequenzy.com/dashboard/company/company_123/forms/form_123"
}
],
"url": "https://sequenzy.com/dashboard/company/company_123/forms"
}
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:read" }
```
# Get Popup Runtime
Source: https://docs.sequenzy.com/api-reference/widgets/popup-runtime
GET /api/v1/widgets/popup.js
Load the hosted JavaScript runtime for popup signup widgets.
Load this public JavaScript file from your website to render popup signup
widgets. The runtime reads `window.SequenzyPopupConfig` and submits subscribers
through the public Forms API.
## Request
This endpoint does not require authentication.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/widgets/popup.js"
```
## Usage
```html theme={null}
```
## Responses
```javascript 200 theme={null}
(function () {
"use strict";
// Popup runtime
})();
```
# Get Preferences Token
Source: https://docs.sequenzy.com/api-reference/widgets/preferences-token
POST /api/v1/widgets/preferences/token
Generate a signed token to embed the subscription preferences widget
Generate a signed token that allows you to embed the subscription preferences widget on your website. This widget lets users manage their email subscription preferences directly from your application.
**Security:** Call this endpoint from your backend only. Never expose your API
key to the frontend. The token is valid for 1 hour.
## Request Body
The subscriber's email address. Must be a valid email of an existing
subscriber.
## Example
```typescript Node.js SDK theme={null}
import Sequenzy from "sequenzy";
const client = new Sequenzy("YOUR_API_KEY");
const { data, error } = await client.widgets.preferences.generateToken({
email: "user@example.com",
});
if (data) {
// Use the token in your frontend iframe
const widgetUrl = `https://sequenzy.com/embed/preferences?token=${data.token}`;
}
```
```bash cURL theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/widgets/preferences/token" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
```
## Using the Token
Once you have the token, embed the widget in your frontend:
```html theme={null}
```
## Responses
```json 200 theme={null}
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
```json 400 Invalid email theme={null}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format"
}
}
```
```json 401 Unauthorized theme={null}
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
}
}
```
```json 404 Not found theme={null}
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Subscriber not found"
}
}
```
```json 500 Server error theme={null}
{
"success": false,
"error": {
"code": "SERVER_ERROR",
"message": "Server configuration error"
}
}
```
The subscriber must already exist in Sequenzy. If the subscriber doesn't
exist, create them first using the [Create
Subscriber](/api-reference/subscribers/create) endpoint.
# Saved Form Embed Script
Source: https://docs.sequenzy.com/api-reference/widgets/saved-form-embed-script
GET /api/v1/forms/{formId}/embed.js
Load a saved signup form with one line of JavaScript.
Load a saved signup form created in the dashboard with one script tag. No API key is required.
The script renders the current saved form settings when the page loads, so edits to the form in Sequenzy apply to deployed JavaScript embeds without copying new HTML.
This endpoint returns `410 Gone` with only a no-op warning script while the
workspace is connected to Webflow. Use native Webflow form capture instead.
## Request
The saved form ID. You can find it on the Forms screen in the dashboard.
```html theme={null}
```
Existing company-scoped embed script URLs continue to work.
## Responses
```javascript 200 theme={null}
(function () {
var currentScript = document.currentScript;
// Renders the saved form and handles submission.
})();
```
```javascript 404 theme={null}
console.warn("Invalid Sequenzy form configuration.");
```
# Submit Signup Form
Source: https://docs.sequenzy.com/api-reference/widgets/submit-form
POST /api/v1/forms/{companyId}
Submit a public signup form and create or update a subscriber.
You can submit an embedded signup form or popup without an API key. The endpoint accepts `application/x-www-form-urlencoded` form posts and JSON.
Forms created in the dashboard post to the [saved form
endpoint](/api-reference/widgets/submit-saved-form), which stores audience
targeting and success behavior server-side. This company-level endpoint keeps
working for non-Webflow popups and existing deployed embeds.
This endpoint returns `410 Gone` while the workspace is connected to Webflow.
Use explicitly selected native Webflow forms instead of a direct form action.
## Request
Your company ID.
The subscriber's email address.
The subscriber's first name.
The subscriber's last name.
The subscriber's phone number in E.164 or US national format. It is stored on
the base subscriber profile and does not grant SMS consent.
Submit a subscriber custom attribute from an HTML form, for example
`customAttributes[nickname]=Ace`.
JSON alternative for custom integrations.
Comma-separated list IDs. Omit this to use your workspace default lists, or
use an empty `lists=` value to add the subscriber to no lists.
Comma-separated tag IDs to apply to the subscriber.
How to handle an existing contact with the submitted email. Use `skip` to
preserve existing fields, `merge` to fill only missing fields, or `overwrite`
to replace submitted fields. `merge` and `overwrite` require a matching
`duplicateStrategyToken` from the Sequenzy form builder.
Signed token generated by the Sequenzy form builder for the selected
`duplicateStrategy`. Required when `duplicateStrategy` is `merge` or
`overwrite`.
Body alternative to the `duplicateStrategy` query parameter for custom
integrations. Query parameter takes precedence when both are present. `merge`
and `overwrite` require a matching `duplicateStrategyToken`.
Body alternative to the `duplicateStrategyToken` query parameter.
Existing deployed embed format for repeated list IDs.
Existing deployed embed format for repeated tag IDs.
Honeypot field. Leave it empty.
Reserved for Sequenzy's hosted embeds. Company-level submissions never carry
one, so omit it. See [Bot protection](#bot-protection).
`http` or `https` URL, or a domain such as `google.com`, to redirect visitors
to after a successful submission.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/forms/company_123?lists=list_abc123,list_def456&tags=tag_abc123&duplicateStrategy=merge&duplicateStrategyToken=SIGNED_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "email=user@example.com" \
--data-urlencode "customAttributes[nickname]=Ace"
```
## Bot protection
Company-level submissions do not carry the signed token that hosted saved forms
and popups send. They may be asked to confirm by email before they become
active subscribers. When Sequenzy support turns on strict protection for your
workspace after a bot attack, they return the normal success response without
subscribing anyone. For signup forms on your site, use a [saved
form](./submit-saved-form) embed.
## Returning unsubscribed contacts
Submitting again counts as a fresh opt-in. If a contact unsubscribed from all email, a new submission resubscribes them and restores the form's lists, whatever the `duplicateStrategy`. When workspace double opt-in is on, they get your confirmation email instead and are resubscribed once they confirm. Contacts whose address bounced or who marked your email as spam are not resubscribed. The response is the same in every case, so it never reveals whether an address was subscribed.
## Responses
```json 200 theme={null}
{"success": true}
```
```text 303 theme={null}
Location: https://example.com/thank-you
```
```json 400 theme={null}
{ "success": false, "message": "Please enter a valid email address" }
```
```json 400 theme={null}
{
"success": false,
"message": "Please enter a valid redirect URL"
}
```
```json 400 theme={null}
{ "success": false, "message": "This email address cannot be subscribed" }
```
```json 404 theme={null}
{ "success": false, "message": "Invalid form configuration" }
```
```json 429 theme={null}
{ "success": false, "message": "Too many requests. Please try again later." }
```
```json 500 theme={null}
{ "success": false, "message": "An error occurred. Please try again." }
```
# Submit Saved Form
Source: https://docs.sequenzy.com/api-reference/widgets/submit-saved-form
POST /api/v1/forms/{formId}
Submit a saved signup form and create or update a subscriber.
You can submit a saved signup form created in the dashboard without an API key. The endpoint accepts `application/x-www-form-urlencoded` form posts and JSON.
The form's stored settings are the source of truth for audience targeting (lists, tags) and success behavior (success message or redirect URL). When you change those settings in the dashboard, deployed embeds pick them up automatically - no re-embedding needed. List, tag, and redirect values in the request are ignored.
This endpoint returns `410 Gone` while the workspace is connected to Webflow.
Webflow-connected workspaces accept only configured native Webflow form
submissions delivered by signed Webflow webhooks.
## Request
The saved form ID. You can find it in the form's embed code on the Forms
screen.
The subscriber's email address.
The subscriber's first name.
The subscriber's last name.
The subscriber's phone number when the saved form includes a phone field. It
is normalized onto the base subscriber profile without changing SMS consent.
Value for a custom field configured on the saved form. The `fieldKey` is
stored as a subscriber custom attribute, for example
`customAttributes[nickname]=Ace`.
JSON alternative for custom integrations. Saved forms only accept keys that
are configured as custom fields on the form. A hidden field that has a
configured value always stores that value and ignores the submitted one; a
hidden field with no configured value stores what you submit. See [Update
Saved Form](/api-reference/widgets/update-saved-form) for the field schema.
Honeypot field. Leave it empty.
Signed bot-protection token. The hosted embed script adds it automatically;
custom integrations can omit it. See [Bot protection](#bot-protection).
Generated saved-form HTML renders built-in name fields first, then email, then
configured custom fields before the submit button. Custom fields cannot
currently be interleaved with built-in fields.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/forms/form_abc123" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "email=user@example.com" \
--data-urlencode "customAttributes[nickname]=Ace"
```
Existing company-scoped saved form submission URLs continue to work.
## Bot protection
The hosted embed script sends a signed `sequenzyToken` with each submission. It proves the
signup came from a page that loaded your Sequenzy form, so Sequenzy can tell it
apart from scripts that post directly to this endpoint.
* You do not need to send a token from your own HTML or server code.
* Submissions without a valid token may be asked to confirm by email before
they become active subscribers.
* When Sequenzy support turns on strict protection for your workspace after a
bot attack, submissions without a token, or with a token issued for a
different form, return the normal success response without subscribing
anyone. Strict protection is only turned on
when your signups all arrive through hosted embed scripts and popups. The
native HTML and fetch snippets from the form code dialog do not send a
token.
## Returning unsubscribed contacts
Submitting again counts as a fresh opt-in. If a contact unsubscribed from all email, a new submission resubscribes them and restores the form's lists, whatever the `duplicateStrategy`. When the form's `resubscribeBehavior` is `double_opt_in`, or workspace double opt-in is on, they get your confirmation email instead and are resubscribed once they confirm. Contacts whose address bounced or who marked your email as spam are not resubscribed. The response is the same in every case, so it never reveals whether an address was subscribed.
## Responses
Native HTML form posts that accept `text/html` receive a hosted confirmation page that mirrors the form's success screen (checkmark, heading, and message) instead of JSON. Forms configured to redirect respond with a `303` to the stored redirect URL.
```json 200 theme={null}
{"success": true}
```
```text 303 theme={null}
Location: https://example.com/thank-you
```
```json 400 theme={null}
{ "success": false, "message": "Please enter a valid email address" }
```
```json 400 theme={null}
{ "success": false, "message": "This email address cannot be subscribed" }
```
```json 404 theme={null}
{ "success": false, "message": "Invalid form configuration" }
```
```json 429 theme={null}
{ "success": false, "message": "Too many requests. Please try again later." }
```
```json 500 theme={null}
{ "success": false, "message": "An error occurred. Please try again." }
```
# Submit Saved Popup
Source: https://docs.sequenzy.com/api-reference/widgets/submit-saved-popup
POST /api/v1/forms/popups/{popupId}
Submit a saved popup and create or update a subscriber.
Saved popup embeds submit here automatically. The popup's stored content controls lists, tags, duplicate handling, resubscribe behavior, custom fields, and redirect behavior.
This endpoint returns `410 Gone` while the workspace is connected to Webflow.
Previously deployed popup scripts are no-ops; configure native Webflow form
capture instead.
## Request
The saved popup ID.
The subscriber's email address.
The subscriber's first name, when the popup includes that field.
The subscriber's last name, when the popup includes that field.
The subscriber's phone number when the popup includes that field. It is
normalized onto the base subscriber profile without changing SMS consent.
Value for a custom popup field, stored as a subscriber custom attribute.
Honeypot field. Leave it empty.
Signed bot-protection token. The hosted popup script adds it automatically;
custom integrations can omit it. See [Bot protection](#bot-protection).
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/forms/popups/popup_abc123" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "email=user@example.com" \
--data-urlencode "customAttributes[company_size]=42"
```
## Bot protection
The hosted popup script sends a signed `sequenzyToken` with each submission. It proves the
signup came from a page that loaded your Sequenzy popup, so Sequenzy can tell it
apart from scripts that post directly to this endpoint.
* You do not need to send a token from your own HTML or server code.
* Submissions without a valid token may be asked to confirm by email before
they become active subscribers.
* When Sequenzy support turns on strict protection for your workspace after a
bot attack, submissions without a token, or with a token issued for a
different popup, return the normal success response without subscribing
anyone. Strict protection is only turned on
when your signups all arrive through hosted embed scripts and popups. The
native HTML and fetch snippets from the form code dialog do not send a
token.
## Returning unsubscribed contacts
Submitting again counts as a fresh opt-in. If a contact unsubscribed from all email, a new submission resubscribes them and restores the popup's lists, whatever the `duplicateStrategy`. When the popup's `resubscribeBehavior` is `double_opt_in`, or workspace double opt-in is on, they get your confirmation email instead and are resubscribed once they confirm. Contacts whose address bounced or who marked your email as spam are not resubscribed. The response is the same in every case, so it never reveals whether an address was subscribed.
## Responses
```json 200 theme={null}
{"success": true}
```
```json 200 theme={null}
{ "success": true, "redirectUrl": "https://example.com/thank-you" }
```
```json 400 theme={null}
{ "success": false, "message": "Please enter a valid email address" }
```
```json 404 theme={null}
{ "success": false, "message": "Invalid popup configuration" }
```
```json 429 theme={null}
{ "success": false, "message": "Too many requests. Please try again later." }
```
```json 500 theme={null}
{ "success": false, "message": "An error occurred. Please try again." }
```
# Update Popup
Source: https://docs.sequenzy.com/api-reference/widgets/update-popup
PATCH /api/v1/popups/{popupId}
Update, publish, or unpublish a saved popup
Update a saved popup. Only the fields you send change.
Set `status` to `published` to make the popup live, or `draft` to stop it
showing while keeping the popup, its stats, and its embed script. The
`trigger`, `targeting`, `schedule`, `frequency`, and `visual` objects are merged
key by key, so patching one key keeps the rest.
For a Webflow-connected workspace, published-popup responses contain
`embed.disabled: true` and the native Webflow settings path instead of custom
code.
## Request
Internal popup name.
`draft` or `published`.
Replacement list targeting. Pass an empty array to capture into every list.
Replacement tag IDs. Pass an empty array to clear tags.
`skip`, `merge`, or `overwrite`.
What happens when a contact who unsubscribed from all email submits this popup
again. `reactivate` resubscribes them and restores the popup's lists.
`double_opt_in` sends your double opt-in confirmation email first and
resubscribes them once they confirm. Turning on `double_opt_in` needs a double
opt-in confirmation email and a sender profile; otherwise the request returns
`400`. If the email or sender is removed later, returning unsubscribed
contacts stay unsubscribed until you fix it. When workspace double opt-in is
on, contacts always confirm first. `duplicateStrategy` never changes
subscription status.
New text for the popup's first heading block. Fails when the popup has no
heading block; edit `blocks` instead.
New text for the popup's first paragraph block. Fails when the popup has no
paragraph block; edit `blocks` instead.
Submit button label.
Success screen message.
HTTP or HTTPS success redirect. Pass an empty string to switch back to the
confirmation message.
`modal`, `slide-in`, `floating-bar`, or `fullscreen`.
`center`, `left`, `right`, `top`, or `bottom`.
Partial trigger patch, merged into the current trigger.
Partial targeting patch, merged into the current targeting.
Partial schedule patch, merged into the current schedule.
Partial frequency patch, merged into the current frequency.
Partial visual patch, merged into the current visual. The `image` style
requires an https `imageUrl` - the image panel is painted as a background, so
a missing or blocked image would leave an empty column beside your content.
Theme overrides merged into the current theme.
Complete replacement for the popup's content blocks. Read the current blocks
with [Get Popup](/api-reference/widgets/get-popup) first. The popup must keep
exactly one required email field and one submit button. See [Content
blocks](#content-blocks) below.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/popups/pop_123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"headline": "Take 15% off your first order",
"targeting": { "paths": ["/pricing"], "device": "desktop" }
}'
```
Unpublish a popup without deleting it:
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/popups/pop_123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "draft" }'
```
## Content blocks
Blocks render in array order. Every block needs an `id` that is unique across
the entire block tree and a `kind`.
Content blocks may also set `sectionId` to `"success"` or `"error"` to render
inside that screen. Input blocks use `kind: "form-field"` and follow the same
shape as [saved forms](/api-reference/widgets/update-saved-form#form-field-blocks).
Properties outside a kind's list are rejected with a named error rather than
dropped, so a typo is reported instead of saving an empty block.
| Kind | Properties |
| - | - |
| `group` | `label`, `layout` (`stack` \| `row` \| `grid` \| `overlay`), `columns` (1-4), `gap` (0-64), `padding` (0-64), `overlayColor`, `overlayPosition` (`top` \| `center` \| `bottom`), `overlayShade` (0-100), `children` |
| `heading` | `content` (required), `level` (1-3), `align` |
| `text` | `content` (required), `variant` (`paragraph` \| `eyebrow` \| `caption`), `align` |
| `image` | `src` (required), `alt`, `fit` (`cover` \| `contain`) |
| `button` | `text` (required), `url`, `variant`, `align` |
| `divider` | none |
| `spacer` | `height` (8-160) |
| `custom-html` | `html` (required), `height` (40-1200) |
| `feature-grid` | `features` (`[{ "title": "...", "description": "..." }]`), `columns` (1-4) |
| `testimonial` | `testimonials` (`[{ "quote": "...", "name": "...", "role": "..." }]`) |
| `countdown` | `label`, `endsAt` (required, ISO 8601) |
| `form-step` | `title`, `description`, `blockIds` |
| `submit-button` | `text` (required), `width` |
| `success-screen` | `heading`, `message` |
| `error-state` | `message` |
Use `kind: "group"` to lay out related children as a vertical `stack`, an
equal-column `row`, a fixed-column `grid`, or an image `overlay`. Responsive
rows and grids collapse to one column on small screens; groups may nest up to
three levels. Overlay needs exactly one direct `image` child and supports
`overlayColor`, `overlayShade` (0-100), and `overlayPosition` (`top`, `center`,
or `bottom`). `gap` spaces the foreground children without moving the
background image. Nested images only enable Overlay for their subgroup; zero
or multiple direct images fall back to Stack. IDs remain unique across the
full tree. A group and all descendants must use the same `sectionId` when they
belong to a success or error screen. `form-step`, `success-screen`, and
`error-state` blocks remain at the root. See the [saved form layout group
example](/api-reference/widgets/update-saved-form#layout-groups) for the full
shape.
A `custom-html` block carries its markup in `html` - not `content` - and sizes
its frame with `height` in pixels:
```json theme={null}
{
"id": "code-chip",
"kind": "custom-html",
"sectionId": "success",
"html": "
WELCOME15
",
"height": 90
}
```
## Responses
```json 200 theme={null}
{
"success": true,
"popup": {
"id": "pop_123",
"name": "Welcome offer",
"status": "published",
"viewCount": 1840,
"conversionCount": 96
},
"embed": {
"scriptUrl": "https://sequenzy.com/embed/popups/pop_123?v=capture-runtime-20260716-14"
},
"message": "Popup updated."
}
```
```json 400 theme={null}
{ "error": "Click-triggered popups require a click selector." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:write" }
```
```json 404 theme={null}
{ "error": "Popup not found" }
```
```json 500 theme={null}
{ "error": "Failed to update popup." }
```
# Update Saved Form
Source: https://docs.sequenzy.com/api-reference/widgets/update-saved-form
PATCH /api/v1/forms/{formId}
Update a saved form's name, audience, copy, theme, or content blocks
Update an existing saved form. Every field is optional - send only what should
change. Use this to retarget the audience, edit copy, restyle the form to
match your brand, or replace the content blocks (for example to remove blocks
you no longer want rendered publicly).
For a Webflow-connected workspace, published-form responses do not return
custom embed code. Their `embed` object contains `disabled: true` and the
native Webflow settings path.
## Request
Saved form ID.
New internal form name.
Replacement list targeting. Every submission joins these lists.
Replacement tag IDs. Pass an empty array to clear tags.
`skip`, `merge`, or `overwrite`.
What happens when a contact who unsubscribed from all email submits this form
again. `reactivate` resubscribes them and restores the form's lists.
`double_opt_in` sends your double opt-in confirmation email first and
resubscribes them once they confirm. Turning on `double_opt_in` needs a double
opt-in confirmation email and a sender profile; otherwise the request returns
`400`. If the email or sender is removed later, returning unsubscribed
contacts stay unsubscribed until you fix it. When workspace double opt-in is
on, contacts always confirm first. `duplicateStrategy` never changes
subscription status.
New text for the form's first heading block. Fails if the form has no heading
block - edit `blocks` instead.
New text for the form's first paragraph block. Fails if the form has no
paragraph block - edit `blocks` instead.
New submit button label.
New confirmation message on the success screen.
HTTP or HTTPS success redirect. Pass an empty string to switch back to the
confirmation message.
Visual overrides merged into the current theme. Any subset of `accentColor`,
`backgroundColor`, `textColor`, `mutedTextColor`, `cardColor`, `borderColor`
(all `#rrggbb`), `borderRadius` (0-32), `headingFontFamily`, `bodyFontFamily`,
and `density` (`compact` | `balanced` | `spacious`).
Full replacement for the form's content blocks. Read the current blocks with
`GET /api/v1/forms` at `forms[].content.blocks`, modify them, and send the
complete array. The form must keep exactly one required email field and one
submit button. See [Content blocks](#content-blocks) below.
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/forms/form_123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"buttonText": "Get the planner",
"theme": { "accentColor": "#0f766e" }
}'
```
## Content blocks
Blocks render in array order. Every block needs an `id` that is unique across
the entire block tree and a `kind`.
Content blocks (`heading`, `text`, `image`, `button`, `divider`, `spacer`,
`custom-html`, `feature-grid`, `testimonial`, `countdown`) may also set
`sectionId` to `"success"` or `"error"` to render inside that screen; input
blocks, the submit button, and the success and error blocks always belong to
the form itself.
Available kinds: `group`, `form-field`, `form-step`, `submit-button`,
`success-screen`, `error-state`, plus the content kinds above.
Properties outside a kind's list are rejected with a named error rather than
dropped, so a typo is reported instead of saving an empty block.
### Content block properties
| Kind | Properties |
| - | - |
| `group` | `label`, `layout` (`stack` \| `row` \| `grid` \| `overlay`), `columns` (1-4), `gap` (0-64), `padding` (0-64), `overlayColor`, `overlayPosition` (`top` \| `center` \| `bottom`), `overlayShade` (0-100), `children` |
| `heading` | `content` (required), `level` (1-3), `align` |
| `text` | `content` (required), `variant` (`paragraph` \| `eyebrow` \| `caption`), `align` |
| `image` | `src` (required), `alt`, `fit` (`cover` \| `contain`) |
| `button` | `text` (required), `url`, `variant`, `align` |
| `divider` | none |
| `spacer` | `height` (8-160) |
| `custom-html` | `html` (required), `height` (40-1200) |
| `feature-grid` | `features` (`[{ "title": "...", "description": "..." }]`), `columns` (1-4) |
| `testimonial` | `testimonials` (`[{ "quote": "...", "name": "...", "role": "..." }]`) |
| `countdown` | `label`, `endsAt` (required, ISO 8601) |
| `form-step` | `title`, `description`, `blockIds` |
| `submit-button` | `text` (required), `width` |
| `success-screen` | `heading`, `message` |
| `error-state` | `message` |
Rich-text limits apply to visible text after stripping HTML and trimming:
heading `content` 1-180 characters, text `content` up to 700, feature `title`
up to 120 and `description` up to 300, testimonial `quote` 1-500, and
success-screen `heading` up to 120 and `message` 1-240. Inline HTML is
sanitized. The raw markup may use up to five times the visible-text limit
plus 500 characters. A feature must have visible text in its title or description.
### Layout groups
Use `kind: "group"` to keep related sibling blocks together and control their
responsive layout. `stack` uses one column, `row` gives every child an equal
column, and `grid` uses `columns`. Responsive rows and grids collapse to one
column on small screens. `overlay` uses exactly one direct `image` child as the
background and places the remaining children above it. Use `overlayColor`,
`overlayShade` (0-100), and `overlayPosition` (`top`, `center`, or `bottom`) to
control its treatment. `gap` spaces the foreground children without moving the
background image. Images inside subgroups only enable Overlay for that subgroup;
zero or multiple direct images fall back to Stack.
Groups may nest up to three levels. A group and all descendants must belong to
the same form state: either omit `sectionId` throughout for the main form, or
set the same `"success"` or `"error"` value on the group and every descendant.
`form-step`, `success-screen`, and `error-state` blocks must remain at the root.
```json theme={null}
{
"id": "contact-row",
"kind": "group",
"label": "Contact details",
"layout": "row",
"columns": 2,
"gap": 12,
"padding": 0,
"children": [
{
"id": "first-name",
"kind": "form-field",
"fieldType": "text",
"name": "firstName",
"label": "First name",
"mapsTo": "firstName"
},
{
"id": "email",
"kind": "form-field",
"fieldType": "email",
"name": "email",
"label": "Email",
"required": true,
"mapsTo": "email"
}
]
}
```
A `custom-html` block carries its markup in `html` - not `content` - and sizes
its frame with `height` in pixels:
```json theme={null}
{
"id": "code-chip",
"kind": "custom-html",
"sectionId": "success",
"html": "
WELCOME15
",
"height": 90
}
```
### Form field blocks
`kind: "form-field"` renders one input. Fields that are not mapped to a
built-in subscriber property are stored as subscriber custom attributes keyed
by the block's `name`.
`text`, `email`, `phone`, `number`, `textarea`, `select`, `radio`, `checkbox`,
`consent`, or `hidden`.
The submitted key, and the custom attribute key when `mapsTo` is
`customAttribute`. Must start with a letter and may contain letters, digits,
`_`, `.`, and `-`. Field names must be unique within a form.
`email`, `firstName`, `lastName`, `phone`, or `customAttribute`. The email
field must use `fieldType: "email"` and `required: true`; first and last name
fields must use `fieldType: "text"`; a mapped phone field must use `fieldType:
"phone"`. A form may contain at most one field per built-in target.
Accessible name, also shown above the input unless `showLabel` is `false`.
Placeholder text. Also used as the empty option's label on `select`.
Whether submissions without a value are rejected.
Pre-filled value. On a `hidden` field this is the server-owned value (see
below).
Whether the label renders above the input.
`full` or `half`. Adjacent `half` fields pair side by side and stack on
mobile.
Required for `select`, `radio`, and `checkbox`: `[{ "value": "pro", "label":
"Pro", "id": "pro" }]`. Only `value` is required - `label` and `id` default
to `value`. Values and ids must be unique within the field, and submissions
outside the option values are rejected. A `checkbox` field stores an array of
the selected values.
### Hidden fields
A `hidden` field with a non-empty `defaultValue` always stores that
server-owned value and ignores whatever the page posts, which is how you stamp
a constant such as a form source. A `hidden` field with an empty
`defaultValue` stores the value the page submits, which is how you pass
machine-filled data such as UTM parameters or a page URL. A required `hidden`
field must have a `defaultValue`.
### Example
```json theme={null}
{
"blocks": [
{
"id": "field-email",
"kind": "form-field",
"fieldType": "email",
"name": "email",
"label": "Email",
"required": true,
"mapsTo": "email"
},
{
"id": "field-plan",
"kind": "form-field",
"fieldType": "select",
"name": "plan",
"label": "Plan",
"placeholder": "Choose a plan",
"mapsTo": "customAttribute",
"options": [{ "value": "starter" }, { "value": "pro" }]
},
{
"id": "field-utm-source",
"kind": "form-field",
"fieldType": "hidden",
"name": "utm_source",
"mapsTo": "customAttribute"
},
{ "id": "submit", "kind": "submit-button", "text": "Subscribe" }
]
}
```
Validation failures name the offending property, for example
`blocks[1].options[0].value: Too small: expected string to have >=1 characters`.
## Responses
```json 200 theme={null}
{
"success": true,
"form": {
"id": "form_123",
"name": "Blog newsletter",
"status": "published",
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123"
},
"embed": {
"actionUrl": "https://api.sequenzy.com/api/v1/forms/form_123",
"scriptUrl": "https://api.sequenzy.com/api/v1/forms/form_123/embed.js"
},
"message": "Form updated."
}
```
```json 400 theme={null}
{ "error": "blocks: Forms must contain exactly one email field." }
```
```json 401 theme={null}
{
"error": "Missing API key. Provide via x-api-key header or Authorization: Bearer "
}
```
```json 403 theme={null}
{ "error": "API key is missing required scope: widgets:write" }
```
```json 404 theme={null}
{ "error": "Form not found" }
```
```json 500 theme={null}
{ "error": "Failed to update form." }
```
# Subscribe to WooCommerce Back-In-Stock
Source: https://docs.sequenzy.com/api-reference/woocommerce/back-in-stock-subscribe
POST /api/woocommerce/back-in-stock/subscribe
Capture a WooCommerce storefront back-in-stock request
You can capture a shopper's back-in-stock request from a WooCommerce storefront. The store must already be connected to Sequenzy, and the request must come from that storefront origin.
## Request
Connected WooCommerce storefront URL, such as `https://store.example.com`.
Sequenzy company ID. Use this when the same storefront URL could match more
than one active connection, or when the storefront URL differs from the
connected WooCommerce site URL.
Shopper email address.
WooCommerce product ID.
WooCommerce variation ID. For simple products, omit this field and Sequenzy
uses `providerProductId`.
Product title to snapshot on the waitlist row.
Variation title to snapshot on the waitlist row.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/woocommerce/back-in-stock/subscribe" \
-H "Content-Type: application/json" \
-H "Origin: https://store.example.com" \
-d '{
"storeUrl": "https://store.example.com",
"email": "buyer@example.com",
"providerProductId": "501",
"providerVariantId": "777",
"productTitle": "Coffee Refill Pack",
"variantTitle": "1kg"
}'
```
## Responses
```json 200 theme={null}
{
"success": true
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid request payload",
"details": "email must be valid"
}
```
```json 400 theme={null}
{
"success": false,
"error": "Invalid WooCommerce store URL"
}
```
```json 403 theme={null}
{
"success": false,
"error": "Invalid storefront origin"
}
```
```json 404 theme={null}
{
"success": false,
"error": "WooCommerce store is not connected"
}
```
```json 409 theme={null}
{
"success": false,
"error": "WooCommerce store connection is ambiguous"
}
```
```json 429 theme={null}
{
"success": false,
"error": "Too many requests"
}
```
```json 500 theme={null}
{
"success": false,
"error": "Failed to subscribe for stock alerts"
}
```
# Authentication
Source: https://docs.sequenzy.com/authentication
How to authenticate with the Sequenzy API
The Sequenzy API uses API keys for authentication. Every request must include your API key in the `Authorization` header.
## Choose the right API key
Sequenzy supports two key types:
* **Workspace API keys** (`seq_live_`) belong to one company. Create them in
**Settings → API Keys & SMTP** for production services that should stay
isolated to that workspace.
* **Account API keys** (`seq_user_`) follow your user account and can access
every workspace you own or have non-restricted access to. Create and revoke
them in **Account Settings → API Keys**. They are useful for administration,
CLI/MCP clients, and multi-workspace automation.
Account keys preserve workspace membership roles: owner and admin memberships
can use granted write permissions, marketer memberships can use marketing
permissions (campaigns, sequences, emails, forms, landing pages, subscribers,
lists, tags, segments, analytics, inbox) but not transactional, settings,
integration, webhook, team, or API-key permissions, and viewer memberships
remain read-only even when the key has a broader preset.
Marketer keys are also refused on workspace-management routes that happen to
sit inside marketing scopes: sending domain creation and verification, landing page custom domains, sequence inbound
webhook secrets, integration details and activity, sync rules, tracking
settings, and transactional delivery data (`emailType=transactional` metrics
and email sends). Subscriber activity, recipient metrics, and audience drilldowns
include only campaign and sequence email data for marketers. Transactional email
templates cannot be reused to create campaigns. These restrictions apply equally
to the dashboard, REST API, CLI, and MCP.
The same source restrictions apply when creating or restarting A/B tests from
existing emails. Campaigns backed by transactional templates are also protected
when reading their audience, changing labels, deleting them, or managing share
links, metrics, recipient drilldowns, and revenue attribution. Existing A/B tests
with transactional-backed variants are protected when reading, editing, copying,
selecting a winner, or rendering previews. Sequences that reference a transactional
template, including alternate email styles or A/B variants, are also hidden from
marketers and cannot be edited, copied, activated, or test-sent by them. Protected
campaign, sequence, and A/B sources remain hidden in sent-email history and event
details. Sequence restrictions also cover analytics leaderboards, recipient drilldowns,
revenue reports, and subscriber enrollment summaries. These rules also apply to incoming email links when editing a sequence.
Marketers can still test marketing emails, unschedule campaigns, and read existing
sending domains to choose a sender. Workspace API key creation requires an owner
or admin; marketers can use an account key through their own account settings.
Account keys use the same permission presets as workspace keys. Their selected
permissions apply in every accessible workspace, so prefer the narrowest preset
that fits the automation.
## Getting Your API Key
1. Log in to your [Sequenzy dashboard](https://www.sequenzy.com/dashboard)
2. Open **Settings → API Keys & SMTP** for a workspace key, or **Account
Settings → API Keys** for an account key
3. Click **Create API Key**
4. Give your key a descriptive name (e.g., "Production Backend", "AI Drafting Agent")
5. Choose a permission preset or custom permissions
6. Copy and securely store your key. It won't be shown again
**Keep your API key secret.** Never expose it in client-side code, public
repositories, or logs. Treat it like a password.
## Using Your API Key
Include your API key in the `Authorization` header with every request:
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
### Example Request
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer seq_live_abc123..."
```
### Select a workspace with an account key
Pass the target workspace in `x-company-id` whenever you use a `seq_user_`
account key:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer seq_user_abc123..." \
-H "x-company-id: company_abc123"
```
The same account key works for newly created workspaces as soon as your account
can access them. If you omit `x-company-id`, Sequenzy selects the first
accessible workspace; multi-workspace integrations should always send the
header explicitly.
### Example in Code
```javascript theme={null}
const response = await fetch("https://api.sequenzy.com/api/v1/subscribers", {
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
});
```
## API Key Best Practices
### 1. Use Environment Variables
Never hardcode API keys in your source code:
```javascript theme={null}
// Good
const apiKey = process.env.SEQUENZY_API_KEY;
// Bad
const apiKey = "seq_live_abc123..."; // Don't do this!
```
### 2. Create Separate Keys for Each Environment
* **Production key**: Used only on production servers
* **Development key**: Used for local development and testing
* **CI/CD key**: Used for automated testing (if needed)
This way, if a development key is compromised, your production data stays safe.
### 3. Rotate Keys Periodically
If you suspect a key has been compromised:
1. Create a new API key
2. Update your application to use the new key
3. Delete the old key
Key validation results are cached briefly for performance, so a deleted key can keep working for up to five minutes before requests start failing.
Rotation is for compromised keys. If a key simply has the wrong permissions,
edit them in place instead - from Settings, the CLI (`sequenzy api-keys update`),
[the API](/api-reference/api-keys/update), or MCP's `update_api_key`. The key
value stays the same, so nothing holding it needs to be re-wired, and widened
permissions apply on the next retry. Removed permissions may remain usable for
up to five minutes while API caches expire.
### 4. Use the Smallest Permission Set
When creating a key, choose the narrowest preset that fits the integration:
* **Read-only** for reporting, dashboards, and audits
* **Safer agent access** for AI agents that should inspect data, define lists and tags, tag existing contacts, set up the workspace, draft content, and edit sequence A/B variant copy without changing list membership, sending live emails, or deleting records
* **AI drafting** for agents that can inspect data, set up the workspace, and draft content but must not send real emails
* **Data ingest, no automations** for syncing contacts and events without starting automations
* **Transactional sender** only for backend services that need to send transactional emails
Use custom permissions when a preset is close but too broad. Sending real emails requires explicit delivery permissions such as `campaigns:send`, `sequences:activate`, `sequences:enroll`, `transactional:send`, `automations:trigger`, or `conversations:write`. Destructive deletes require explicit `*:delete` permissions.
Some permissions are deliberately narrow so a read-mostly key does not have to
be widened to do one job. `subscribers:tag` adds and removes tags on contacts
that already exist without granting the ability to create, update, or
unsubscribe them; `subscribers:write` includes it. `lists:write` and
`tags:write` define lists and tags but do not change who belongs to them -
adding contacts to a list or removing them from it requires `subscribers:write`
as well, and deleting the list or tag requires `lists:delete` or `tags:delete`.
### 5. Never Commit Keys to Git
Add your environment file to `.gitignore`:
```bash theme={null}
# .gitignore
.env
.env.local
.env.production
```
### 6. Use Secrets Management in Production
For production deployments, use your platform's secrets management:
* **Vercel**: Environment Variables in dashboard
* **AWS**: AWS Secrets Manager or Parameter Store
* **Heroku**: Config Vars
* **Docker**: Docker Secrets or environment variables
## Authentication Errors
### 401 Unauthorized
```json theme={null}
{
"success": false,
"error": "Unauthorized"
}
```
This error occurs when:
* The API key is missing from the request
* The API key is invalid or has been deleted
* The API key format is incorrect
**Solution**: Check that you're including the `Authorization: Bearer YOUR_KEY` header with a valid key.
### Common Mistakes
| Mistake | Correct Format |
| - | - |
| Missing "Bearer" prefix | `Authorization: Bearer seq_live_...` |
| Exposing key in URL | Pass the key in a header, never in query params |
Sequenzy also accepts the key in an `X-API-Key` header, but prefer
`Authorization: Bearer` so examples and tooling stay consistent. There are no
separate test keys: workspace keys start with `seq_live_`, account keys with
`seq_user_`, and older workspace keys with `ek_` still work.
## Managing API Keys
### View Active Keys
In **Settings → API Keys & SMTP** or **Account Settings → API Keys**, you can
see:
* Key name
* Created date
* Last used timestamp
### Delete a Key
To revoke access:
1. Go to the API Keys page where the key was created
2. Find the key you want to delete
3. Click **Delete**
4. Confirm the deletion
Deleting a key immediately invalidates it. Any requests using that key will
start failing.
## Security Recommendations
### Server-Side Only
Only use API keys in server-side code. Never expose them to browsers:
```javascript theme={null}
// Server-side (Node.js, Next.js API route) - Safe
export async function POST(request) {
const response = await fetch("https://api.sequenzy.com/api/v1/...", {
headers: { Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}` },
});
}
// Client-side (React component) - NEVER do this
function Component() {
// This exposes your key to anyone viewing the page source!
fetch("https://api.sequenzy.com/api/v1/...", {
headers: { Authorization: "Bearer seq_live_..." }, // DANGEROUS!
});
}
```
### Use HTTPS Only
Always use `https://` when making API requests. The API does not accept unencrypted HTTP connections.
### Monitor Usage
Regularly check the "Last used" timestamp for your API keys. If you see unexpected activity, rotate the key immediately.
## Next Steps
Explore all available endpoints
Send your first email in 5 minutes
# Changelog
Source: https://docs.sequenzy.com/changelog
What's new in Sequenzy
## Brands like yours match your business
**Brands like yours** in the gallery pickers, the [references API](./api-reference/references/list), `sequenzy references` and MCP now compare your company description with what each gallery brand sells and to whom, instead of what its emails talk about, so an email tool sees other email tools rather than founder newsletters.
## Never send a broken link again
The email check in the editor now opens every link and image in your email and tells you which ones are broken, unreachable, mistyped, missing `https://`, pointing at localhost or a staging site, or still placeholders. It also catches leftover filler text, name tags without a fallback and oversized images, and **Show** jumps to the block to fix. Campaigns now ask **Ready to send?** with the results before going out. Also in the [API](./api-reference/campaigns/check), `sequenzy campaigns check` and the `check_email` MCP tool.
## Wake AI agents when something happens in Sequenzy
The hosted MCP server now supports MCP Events, so a ChatGPT task can react to a reply, bounce, spam complaint, poll answer, new or unsubscribed subscriber, sent campaign, finished sequence or completed import instead of running on a schedule. Ask it something like "When someone replies to a campaign, draft a reply in my CRM." See [MCP Events](./concepts/mcp-events).
## Dead webhook endpoints are disabled automatically
If every delivery to a webhook endpoint fails for 7 days, Sequenzy now disables it, skips its pending deliveries and emails the workspace owner. Webhooks shows why it was disabled, the [webhooks API](./api-reference/webhooks/outbound) returns `failingSince` and `autoDisabledAt`, and `sequenzy webhooks list` prints the reason. Enable the endpoint again once it returns `2xx`.
## Search your settings
Type in the search box at the top of Settings, or press `/`, to jump straight to a setting such as double opt-in, DKIM, the frequency cap, SMTP or your Stripe connection. The command palette finds the same settings, and each result links to the exact section, so you can share it.
## Watch your competitors' emails
Watch a brand from any email in the Gallery tab, and the new **Competitors** page shows every email it sends, a few hours after it goes out. Emails since your last visit are marked new, and you can remix any of them for your brand. Brands not in the gallery yet are requested when you watch them. Also in the [API](./api-reference/references/watchlist-emails), `sequenzy watchlist` and MCP. See [Competitor Watchlist](./concepts/competitor-watchlist).
## Ask for your competitors in the email gallery
Missing a brand you want to learn from? Use **Suggest a brand** in the Gallery tab when adding a new email. Sequenzy signs up to its emails and adds it to the gallery, and you can follow each request until its emails are in. Also in the [API](./api-reference/references/brand-requests-create), `sequenzy brand-requests` and MCP.
## See why gallery emails work, and preview whole sequences for your brand
Email pages in the email gallery now show a breakdown: the main button, length, layout, fonts, voice, when it arrived, who sent it, how it is built and why it works. Sequence pages rewrite the first emails for your website before you sign up, and when you clone a sequence you choose which emails to keep. The [References API](./api-reference/references/list) returns each email's `analysis`, and [Create Sequence from Example](./api-reference/sequences/create-from-example) accepts `stepNumbers`, also in the CLI (`--steps`) and MCP. Previews are in the API too: [Preview Template from Example](./api-reference/templates/preview-from-example) and [Preview Sequence from Example](./api-reference/sequences/preview-from-example), `preview-example` in the CLI, and MCP.
## CSV imports keep unsubscribed contacts unsubscribed
When you import a CSV exported from AWeber, Mailchimp, Kit, MailerLite, Brevo, ActiveCampaign or beehiiv, the importer now reads its status and unsubscribe date columns, keeps unsubscribed and bounced contacts unmailable, and shows how many contacts land in each status before you import.
## Clearer tool names in ChatGPT
On the ChatGPT connector (`/v1/mcp/openai`), `list_websites`, `check_website` and `resend_campaign_to_non_openers` are now `list_sending_domains`, `get_sending_domain` and `create_non_opener_resend_draft`, `edit_sequence_graph` is split into `move_sequence_step`, `duplicate_sequence_step`, `delete_sequence_step` and `replace_sequence_paths`, and `add_website` and `submit_feedback` are no longer offered. Other MCP clients keep the original tools. See [MCP Tools](./concepts/mcp-tools).
## One CNAME for your bounce records
New sending domains publish their bounce subdomain as a single CNAME instead of an SPF TXT and an MX record, so domains on DNS hosts that block custom MX records, such as Namecheap with email forwarding on, can now verify. Domains you added earlier keep their SPF TXT and MX records. See [Domain Verification](./guides/domain-verification#unified-record-set-for-new-domains).
## Edit translations by hand and keep your edits
Open **Translations** in the email editor and pick a language to edit its wording directly. Automatic translation no longer replaces a translation you edited: when the original changes, it's marked **Needs review** instead. The campaign review step now warns about outdated or missing translations, and **Settings → Localization** shows which language each subscriber value maps to. The API, CLI and MCP gain opt-in `keepEdits` and `skipEdited` options and an `editedAt` field; existing calls behave as before. See [Email Localization](./concepts/email-localization).
## Start new emails from brands like yours
When you add a campaign, transactional email or sequence, the new **Gallery** tab shows real emails from the brands most like yours, matched to your company description, or switch to **All examples** to search the whole email gallery. Pick one and Sequenzy remixes it for your brand. Also in the [References API](./api-reference/references/list), `sequenzy references` and MCP.
## Browse real email examples from hundreds of brands
The [email examples gallery](https://www.sequenzy.com/email-examples) is now public. Browse real welcome, onboarding, sale and transactional emails by brand, type, color, style or season, see each brand's sending facts and first 30 days, and open whole sequences day by day. Remix any email or clone a sequence into your workspace, from the gallery or with the API, CLI and MCP. You can now also pass the URL of an email shown inside its sequence (ending in `#email-{slug}`) when you remix it.
## Recover each failed Stripe invoice separately
Stripe failed payment events now carry the invoice ID, the attempt count, the amount due, a link to pay the invoice and when Stripe retries next. Use them to give each failed invoice its own recovery run that ignores automatic retries and stops when that invoice is paid. In the builder, turn on the **One run per failed invoice** switch on a `saas.payment_failed` trigger to set it up. See [Failed Payment Recovery](./integrations/stripe#failed-payment-recovery).
## Web and mobile push notifications
Send push notifications to browsers and to your iOS and Android apps from campaigns and sequences. Each platform tab in `Settings -> Push` shows its status and has a **Copy prompt** button for your AI coding tool, and web push uses your logo and a no-code subscribe button. Push campaigns and **Send Push** steps report sent, displayed and clicked counts, segments can filter on push devices and activity, and everything works in the API, CLI and MCP. See [Push notifications](./concepts/push-notifications).
## Your inbox now has its own address
Anyone, including other AI agents, can email `inbox+{companyId}@inbound.sequenzy.com`, or a branded address like `support@inbound.yourdomain.com`, and the message lands in Inbox next to replies. Inbox settings have their own **Settings → Inbox** tab. Replies to your team's replies now stay in one conversation, attachments can be downloaded, forwarded and sent with replies, and you can forward messages and mark conversations unread. Received email is kept for at least 90 days. The API, CLI and MCP gain the same actions. See [Inbox](./concepts/reply-tracking).
## Sync contacts and events from your data warehouse
Warehouse Sync runs a SQL query on Snowflake, BigQuery, Redshift or Postgres on a schedule and imports contacts or records events on existing contacts. Each run sends only rows that changed, and a cursor column such as `updated_at` keeps large tables fast. Set it up in **Settings > Data Warehouse**, or with the new [Warehouse Sync API](./api-reference/warehouse/syncs-create), `sequenzy warehouse` and MCP tools. See [Warehouse Sync](./integrations/warehouse-sync).
## Start a sequence when someone leaves a segment
The new **Segment exited** trigger enrolls active contacts who stop matching a segment, for win-back or downgrade flows. Unsubscribing or bouncing doesn't count as leaving, and nobody is enrolled when you turn the sequence on. Available in the builder, API, CLI and MCP. See [Segment-Exited Trigger Behavior](./concepts/sequences#segment-exited-trigger-behavior).
## Frequency cap for marketing emails
Set a workspace rule in **Settings > Tracking** (Frequency Cap section), such as at most 1 marketing email every 16 hours, then turn on **Respect frequency cap** in the sequences that should follow it. Contacts already at the cap skip that email and move on to the next step. Campaigns count toward the cap, and transactional emails are never skipped. Available in the API, CLI and MCP. See [Frequency Cap](./concepts/sequences#frequency-cap).
## Track page views from your website
Call `sequenzy.page()` in the browser SDK to record `web.page_viewed` for identified visitors, with the page URL, path and title. Use it as a sequence trigger with a filter on the page path, for example to follow up with visitors who viewed your pricing page. See [Website Tracking](./concepts/web-tracking#page-views).
## See which email clients open your emails, and whether A/B results are real
Campaign reports and the Metrics page now show which mail clients and devices open your emails, so you know where to test your designs. A/B test results also tell you whether the leading variant's advantage is statistically significant or could still be chance. Both are available through the API, CLI and MCP. See [Metrics](./concepts/metrics#which-email-clients-opened) and [Campaigns](./concepts/campaigns#is-the-difference-real).
## Stream your data to S3 or Google Cloud Storage
Data exports write email events, SMS events, custom events and a daily subscriber snapshot to a bucket you own, as often as every 5 minutes. Files are gzipped JSON lines in date partitions, ready to load into Snowflake, BigQuery, Redshift or DuckDB. Set it up in **Settings > Data Warehouse** (Export to bucket), or with the new [Data Exports API](./api-reference/data-exports/create), `sequenzy data-exports` and MCP tools. See [Data Exports](./integrations/data-exports).
## Safer editing across the email, sequence and page builders
Saving a published landing page now asks before updating the live page, and saves no longer overwrite a teammate's newer version. Repeat block item values are inserted as text; use `{{html.item.field}}` for HTML. Content conditions compare dates in order, numbers by value and list attributes by item, and a column or group text color now applies to the text inside it. Popup paths ending in `$` match only that exact page. See [Landing pages](./concepts/landing-pages) and [Repeat Blocks](./guides/email-editor#repeat-blocks).
## One tracking domain for all your sending domains
Tracked links now use one company tracking domain, such as `links.yourdomain.com`, for every sending domain, so you add its CNAME once. It never blocks sending: until it works, or if its record breaks, links use Sequenzy's shared tracking domain, and the dashboard alerts you when a working domain stops. Manage it in **Settings > Tracking** or with the new [Tracking Domain API](./api-reference/tracking-domain/get), CLI and MCP tools. See [Custom Tracking Domains](./guides/domain-verification#custom-tracking-domains).
## The local MCP server starts before you add an API key
If you install the Sequenzy MCP server without `SEQUENZY_API_KEY`, it now starts and lists its tools instead of exiting. Each tool call explains how to create a free account and connect, so your agent can walk you through setup. See [MCP](./concepts/mcp).
## Close inbox conversations in bulk
You can now select several inbox conversations, or a whole page with one checkbox, and close or reopen them together. The API accepts up to 100 conversation IDs per request, `sequenzy inbox close` and `reopen` take several IDs, and MCP adds `bulk_update_conversation_status`. See [Update Conversation Statuses](./api-reference/conversations/bulk-update-status).
## Choose who a reply comes from
The inbox composer now shows **From** and **To** rows, and you can reply from any sender profile on a verified domain. It starts with the address the subscriber is already talking to when that address is one of your sender profiles. The API, CLI and MCP accept `senderProfileId` for the same choice. See [Reply to Conversation](./api-reference/conversations/reply).
## Ask AI is now Call AI, with a faster way to test it
The sequence step that writes a personalized line per contact is now called **Call AI**. Its editor keeps a Test panel beside the prompt: press `Cmd+Enter` to run on up to three real contacts, compare runs word by word, see what the AI was given, and undo any AI rewrite or restore. Existing steps keep working unchanged. See [Call AI steps](./concepts/sequences#call-ai-steps).
## Reply previews in the inbox
The inbox list now shows a short preview of each conversation's latest reply, with quoted history removed, and the API, CLI and MCP return it as `lastMessagePreview`. See [List Conversations](./api-reference/conversations/list).
## Custom headers on the Send API
Send Email now accepts a `headers` object, so a transactional send can carry your own `List-Unsubscribe` one-click link for lifecycle mail you manage yourself. Marketing sends keep Sequenzy's signed unsubscribe headers. Headers that cannot be applied no longer disappear silently: the response lists them in `ignoredHeaders`. The CLI adds `--header` and MCP `send_email` accepts `headers`. See [Send Email](./api-reference/transactional/send).
## Every new sending domain gets its own tracking subdomain
Domains you add now send tracked links and opens through their own subdomain, `links.yourdomain.com` by default, instead of a shared host. You add one more CNAME, and the domain verifies once it works. You can also choose the bounce subdomain instead of `send`. Existing domains keep working as they are and can opt into their own tracking subdomain. See [Domain Verification](./guides/domain-verification).
## Attachments up to 15MB
Emails can now carry up to 15MB of attachments in total, up from 7MB, across the API, SMTP, sequence email steps, the CLI and MCP. The limit of 10 files per email is unchanged. Some company mail servers still reject messages over about 10MB, so link to very large files instead. See [Send Email](./api-reference/transactional/send).
## Signing up again resubscribes unsubscribed contacts
When someone who unsubscribed from all your email submits a saved form, popup or landing page again, they are now resubscribed and rejoin its lists, whatever the duplicate strategy. To ask them to confirm by email first, turn on **Require email confirmation to resubscribe** on the form or popup, or set `resubscribeBehavior` to `double_opt_in` in the API, CLI or MCP. See [Returning unsubscribed contacts](./widgets/signup-form#returning-unsubscribed-contacts).
## Strict bot filtering catches Microsoft link checks
Strict bot filtering now also filters opens and clicks from Microsoft Azure IPs, and campaign and sequence clicks that arrive within 60 seconds of delivery from an IP that is also clicking other recipients' emails, so Microsoft Defender checking an email's only link no longer inflates click rates. See [Metrics](./concepts/metrics).
## Wait until a contact's own date
A Delay step set to **Until a date** can now read the date from a contact attribute, such as `dueDate`, as well as from the trigger event. Choose what happens when that date already passed: continue, skip ahead to the next wait, or exit. Contacts can then join a date-based timeline at any point and only receive the emails still ahead of them. In the API, CLI and MCP, use `waitUntil.source` and `waitUntil.pastAction`. See [Sequences](./concepts/sequences#delay-fixed-duration-or-wait-until-a-date).
## Welcome emails for workspaces without a website
Workspaces started without a website now get the same four-email welcome sequence as everyone else, written from your business name and an optional one-line description. Setup is a single screen with an optional logo, which you can also add later from **Add logo** on your dashboard. You can also create these workspaces from the API with `noWebsite` and an optional `description` and `withWelcomeSequence` on [Create Company](./api-reference/companies/create), from the CLI with `sequenzy companies create --without-website`, or with the `create_company` MCP tool.
## Simpler sequence, campaign and settings screens
A sequence now shows one status, and **Pause** always means no emails go out until you resume. Stopping new enrollments is its own choice in the **Pause** menu. **Activate** saves your changes first. In campaigns, each problem that blocks sending links to the step that fixes it. Settings combines **Events & Sync Rules**, **Tags & Labels** and **API Keys & SMTP**, and old links still open the right section.
## Keep testing a sequence A/B test, or change its winner
If a sequence A/B test picked a winner before you were sure, open the step and choose **Resume Testing**: subscribers are split across the variants again, results so far are kept, and the test waits for you to select a winner. The new **Pick winner automatically** setting shows whether a test will choose on its own. **Change Winner** switches to another variant, including after an automatic pick. Both are also in the API ([Resume A/B Test](./api-reference/ab-tests/resume), [Select A/B Test Winner](./api-reference/ab-tests/select-winner)), the CLI and MCP.
## Track emails your code renders
Add `trackAs` to a direct-content send, for example `"trackAs": "weekly-report"`, and every send with that value is counted under one transactional email with its own opens, clicks and bounces. The first send creates it, marked **Code** in your dashboard with a snapshot of recent content. Disable it to stop those sends. Use `--track-as` in the CLI or `trackAs` in the `send_email` MCP tool. See [Send Email](./api-reference/transactional/send#track-emails-rendered-in-your-code).
## Preview accounts before creating them from an organization ID
When your events or contacts carry an organization ID, **Subscribers**, then **Accounts** now previews the biggest organizations it would create, with their contacts, before you click. Accounts without a name property are named after their contacts' shared work email domain instead of a raw ID. Detecting, previewing and creating are also in the API, `sequenzy accounts detect-org-ids` and MCP. See [Accounts](./concepts/accounts).
## Clearer subscriber operation errors
Invalid [Start a Subscriber Operation](./api-reference/subscribers/operation-start) requests now name the failing fields in `error` and list them in `issues`, including the accepted values for fixed choices such as `kind`.
## Clearer segment filter errors
[Create Segment](./api-reference/segments/create) and [Update Segment](./api-reference/segments/update) now check the shape of `root` and `filters` and return `400` with an `issues` entry for each malformed path, such as a group without `children`. Filters inside `root` that are missing `kind` are now accepted as filters. In nested trees, a filter with a missing operator or a number `value` could be ignored, which widened the segment; it now returns `400`.
## Uploaded GIFs keep animating
Animated GIFs and WebP images you upload to the media library now keep every frame. Before this fix, a GIF over 250 KB or wider than 1200 px was replaced in the library with a still JPEG of its first frame, and a large WebP with a still WebP. Emails that already use one of those stills keep showing it. To animate them, upload the GIF again and swap it in. Large still images with transparent areas also no longer get a black background.
## Attribute types stay as you set them
When you add an attribute to a contact, pick one your other contacts already have and the value keeps its type. Each attribute has a string, number or boolean type selector. Zip codes such as `02134` and long IDs now stay strings when you edit a contact or import a CSV in the dashboard. To see existing names and types, call [List Subscriber Attributes](./api-reference/subscribers/attributes), run `sequenzy subscribers attributes`, or use the `list_subscriber_attributes` MCP tool. `--attrs-json` sets typed values from the CLI.
## Deeper segment-of-segment filters
Segments can now reference other segments up to 16 levels deep, and nested filter groups no longer count toward that limit. Counting follows up to 1,000 segment references in total. Past a limit, requests return a `400` naming the segment and reference chain instead of a server error. See [Get Segment Count](./api-reference/segments/count).
## Account suggestions from email domains
When several contacts share a work email domain, **Subscribers**, then **Accounts** now suggests an account for that company. Review the list, uncheck anything that is not a customer, and create the rest in one click. Personal and disposable providers are skipped, and no emails or sequences run. The same suggestions are in the API, `sequenzy accounts suggestions` and MCP. See [Accounts](./concepts/accounts). Accounts also pick up billing from your payment integrations: the paying contact becomes the account's owner, and `plan`, `mrr` and `billing_status` update automatically for every member.
## Clearer sequence API errors
`422` responses from the API now name the failing field and, for fixed choices, the accepted values. `POST /sequences` also returns `warnings` when it ignores a field such as a nested `triggerConfig` or `delay: 2`, when a list ID is not found, or when a `contact_added` trigger has no list. `PUT /sequences/:id` reports ignored fields and unknown lists the same way, and Enable Sequence returns readiness warnings, including when a trigger enrolls every new contact. See [Create Sequence](./api-reference/sequences/create#request-shape-and-warnings).
## Sequence A/B winners use real engagement
Sequence A/B tests now pick the variant with the highest open or click rate once the threshold is reached, excluding bot opens and clicks. Previously the winner could be selected without engagement data. If no variant has opens or clicks yet, or the leaders are tied, recipients keep being split until one variant leads. The leader is rechecked every 5 minutes.
## Run data for audience enrollments
Pass values that are the same for everyone in an enrollment run, such as a sale end time or a discount percentage, when you enroll an audience in a manual-trigger sequence. Add **Run data** in the enroll sheet, `data` on `POST /sequences/:id/enroll-audience` or `enroll_sequence_audience`, or `--data-json` in the CLI. The sequence's emails and conditions read it as `{{enrollment.field}}`, and the editor's variable picker lists the fields of your latest run.
Campaigns that are mid-send when company sending is paused now pause automatically instead of stalling. When sending is restored they resume at their original pacing, so remaining recipients continue at the same rate as before the pause.
## Rate-based complaint limits for high-volume senders
From 20,000 emails, complaint limits are now based on your complaint rate (0.015% warning, 0.03% pause) instead of a fixed count, so a few complaints across a large volume no longer pause healthy sending. Fixed counts still protect accounts below 20,000 emails. See [Sender Health & Thresholds](./guides/sender-health).
## Offer-led Black Friday and seasonal sale emails
Sale countdown emails now lead with the offer and its exact start or end time instead of a generic feature list, follow the phase of the countdown (teaser, launch, final hours, Cyber Monday), and never invent a discount. Add **Your offer** when creating the Black Friday & Cyber Monday template so every email quotes it. Step briefs are no longer echoed into the copy.
Dodo Payments customers whose subscription ends now receive the `churned` tag and the `saas.churn` event. The `cancelled` tag is reserved for live subscriptions scheduled to end at their next billing date. Run **Sync revenue** on your Dodo integration to update contacts that cancelled before this change.
## Form, popup and landing-page submission history
Open **Submissions** on a form, popup or landing page to see accepted answers together, view each submission’s details, filter by a custom answer and export matching records to CSV. Answers stay as submitted even when contact details change. Tracking covers future submissions, with matching API, CLI and standard MCP access; explicitly recovered historical records are marked Reconstructed with their evidence event time.
## Email and sequence previews in your assistant
Review rendered emails in desktop or mobile view and explore your saved sequences as connected flows in MCP Apps clients. Click an email step to see its content, switch between available A/B variants and inspect personalization warnings. Previews use your existing permissions and never activate a sequence or send an email. Compact image-upload and import-progress cards support your workflow in the same Sequenzy style.
## Countdowns: start on a date and keep enrolling late joiners
Audience enrollment for countdown sequences can now start at a moment you pick, and a "keep enrolling people who join later" option adds new list members or segment matches until the last key date, dropping them onto the current step. The sequences list shows each countdown's next moment and whether it is live. Available in the API, CLI and MCP as `scheduledFor` and `audienceAutoEnroll`.
## Member opt-out for private lists
You can now enable **Allow members to unsubscribe** on a private list. Only existing members see the list in email preferences and can opt out; other subscribers cannot discover or join it. The setting defaults to off and is available in the dashboard, API, CLI and MCP.
## More reliable complaint reviews
Complaint reviews now use Luna with a fallback model. If AI returns no usable decision, trusted accounts with verified complaint rates below 0.1% keep their current sending state while existing complaint limits and high-risk safeguards remain in place.
## Countdown sequences with key dates and audience enrollment
Build one-time, calendar-anchored sequences such as a Black Friday countdown. A new manual trigger keeps anyone from entering automatically, key dates on the sequence let every step wait for "7 days before Black Friday starts" or "1 hour before it ends", and moving a date re-times contacts already waiting. Enroll everyone, a list or a segment in the background from the Subscribers dialog, with progress and cancellation. Late enrollees skip the steps whose moment already passed. The Black Friday & Cyber Monday template pre-fills the season and generates copy for each moment. Available in the API, CLI and MCP as `keyDates`, `waitUntilKeyDate` and enroll-audience.
## Recipient limit for campaigns
Cap how many people a campaign reaches. Turn on Recipient limit on the schedule step and Sequenzy sends to the first N matching subscribers after every audience rule and suppression, which makes daily domain ramp-ups, soft launches and batched imports a matter of duplicating the campaign and raising the number. The cap is available as `maxRecipients` in the API, CLI and MCP.
## Custom tracking domains on every plan
Custom tracking domains are now available on the free plan. Add a CNAME under Settings -> Email Tracking so click redirects and open pixels use your own host instead of the shared one, which keeps every link aligned with your sending domain.
## Saved goals in campaigns
Campaign Goals now shows your company-wide Settings goals and their campaign conversions automatically, with the same goal definitions available through the API, CLI and MCP.
## Labels for transactional emails
Organize transactional templates with the same company labels you use for campaigns. Assign or create labels from the row menu, filter by labels, and keep assignments when duplicating. The API, CLI, and MCP support assigning, reading, filtering, and clearing labels.
### See and control running campaign A/B tests
Your campaign now shows what differs between variants, the test audience, delivery progress and the winner selection countdown. You can adjust the audience or waiting period, finish early with a selected winner, and retry interrupted changes. The public API, CLI and MCP support the same controls.
### Easier cart design and event previews
Cart blocks now open with design presets, image and typography controls, and a compact event summary. You can apply suggested fields with explicit price units and preview empty carts, long names, missing images and mobile layouts. API, CLI and MCP clients get the same presets, suggestions and scenario data. Repeat previews also load real event rows without placeholder data getting in the way.
### Create campaigns from a selected group
Choose **Create campaign** from filtered contacts, selected rows or an email activity group, then choose a template. The complete group is saved directly in the draft without creating a list or changing sequence enrollments. Saved lists and segments retain campaign and sequence creation. Sequence activity selections include A/B email steps. Drafts show an **Audience set** chip, and the API, CLI and MCP support the same direct audience selection.
### Lemon Squeezy reconciliation and disconnect
Lemon Squeezy revenue now reconciles refunds and historical imports without counting payments twice, and delayed payments preserve expired subscription state. Imported subscriptions retain the comparison state needed for future trial conversions and plan changes. You can also disconnect Lemon Squeezy through the API, CLI, or MCP and retry managed webhook cleanup if the provider is unavailable.
### Test email delivery history
You can see pending, successful and failed test sends in Sent emails and through the API, CLI and MCP.
### Custom reply domain verification
Custom reply tracking now checks verification of your receiving domain before showing it as active. If a legacy reply domain needs a verification TXT record, you can retrieve it in the dashboard, API, CLI, or MCP and retry verification while keeping existing reply addresses.
### Subscriber import checker removed
CSV imports no longer run list-risk scoring, block entire files based on list patterns or source, or send import-risk alerts. You can import without a safety bypass. Contact validation, permission confirmation, suppression handling, and email-sending safeguards remain in place. If an earlier upload was blocked, upload the file again.
### Rename matching email footers
When you rename your company, you can now update matching company names in editable email footers and saved components at the same time. Custom names and protected campaigns stay unchanged. The same option is available through the API, CLI and MCP.
### Review a whole sequence
Review saved email steps and A/B variants with one sample contact, move between emails while editing, and check timing and branches before activation. Use the existing activation and enrollment controls when you are ready.
### Restored signup list compatibility
Your existing API keys can again create or update subscribers with explicit lists using `subscribers:write`. We reverted the additional `lists:write` requirement introduced on September 8 for `POST /api/v1/subscribers`. You do not need to change your key or integration. Imports into explicit lists and dedicated list membership endpoints keep their existing permission requirements. Review rejected signups before retrying them, since successful requests can trigger onboarding automations.
### Event properties in sequence tests
Test abandoned-cart and other event-driven sequences with your own JSON event properties, including cart IDs, arrays and nested objects. Copy your latest received event payload or enter JSON in the live sequence test dialog, or start and inspect a live test through the API, CLI or MCP. Live tests run real actions for your selected subscriber and mark emails as tests.
### Connect Lemon Squeezy
Connect your Lemon Squeezy store to import customer revenue, keep subscriber metrics up to date, and receive purchase, subscription, refund, and dispute events automatically. You can set it up from the dashboard, API, CLI, or MCP, with managed webhook provisioning available by default.
### Direct unsubscribe links when tracking is off
Turn off unsubscribe tracking to send footer and one-click unsubscribe links directly to sequenzy.com, even when you use a custom tracking domain. The setting applies to subsequent sends, through the dashboard, API, CLI, and MCP. Actual unsubscribes and their email attribution are still recorded.
### Script sends with JSON receipts and piped content
You can send emails from scripts with explicit workspace selection, JSON receipts, and piped HTML. Supported text-file inputs also accept stdin. CLI and MCP failures now include structured recovery information and API retry hints, while CLI diagnostics preserve request keys for interrupted operations.
### A better HTML editing experience
Edit HTML blocks and full email source with syntax highlighting, code completion, folding, find and replace, and undo and redo. Expand the editor for more space without losing your selection or undo history, and search rendered HTML directly from the HTML tab.
### Preview and apply your default footer
You can now apply your default footer to selected sequences, draft or scheduled campaigns, transactional emails, and reusable templates. Review affected counts and before/after previews first, with customized footers preserved by default. The same workflow is available through the API, CLI, and MCP, including HTML previews for agents.
### More reliable Make searches
Sequenzy's Make connector now follows API pagination for campaigns, sequences, subscribers and suppressions, with a configurable result limit on every search. Filtered modules use clearer Search labels, live dropdowns load more choices. Setup guidance also covers list imports and double opt-in delivery.
### Accurate non-opener resend previews
Your non-opener resends now exclude openers across every audience format, including manually added recipients. Counts and previews stay consistent throughout the campaign wizard, and resending a resend preserves earlier opener exclusions.
### Rewrite Ask AI prompts from your goal
Describe what you want to achieve in the Ask AI prompt editor, then choose **Rewrite prompt** to refine the current prompt without first generating a contact preview.
### Signup notifications for recent timestamps
New-subscriber notifications now work when your API, CLI or MCP integration supplies a signup timestamp at most one hour old. Older timestamps and bulk imports remain silent on creation; double opt-in confirmation can still notify.
### Easier dashboard use on your phone
You can configure sequence conditions, review campaigns, inspect personalized email previews, and manage contacts and settings with layouts that fit smaller screens. Contact tabs stay visible, and preview data and deliverability checks are now accessible on mobile. You can expand compact contact rows to see more details.
### Clearer cart and order data settings
Cart and order settings now open on a **Data** tab showing where items come from and whether your preview uses real data or samples. You can map product names, images, quantities, prices, and other item fields to your own data, choose fields from preview examples, and catch missing values or invalid prices inline. The Data tab also shows the triggering event and lets you inspect the items payload. Use **Appearance** to choose what is visible. See [Cart and Order Blocks](/concepts/cart-and-order-blocks).
## Background contact tasks
You can add tags to filtered audiences in the background, keep working while tasks run, and return to check progress, inspect failures or cancel future work. The same workflow is available through the API, CLI and MCP. Contact browsing also avoids unnecessary count requests and uses cursor pagination when sorting by creation date.
### Clearer sequence setup and email previews
Review activation blockers in one checklist and jump to the step that needs attention. Step pickers show where your new step will go, and email steps now offer a quick saved-content preview with an Edit email shortcut.
### A calmer email editor
Block settings open when you select a block on desktop, with unified controls and an animated **Settings / AI** switch that highlights when you pick a block and returns to the assistant when you deselect it. Image pickers keep your settings selected. Discard offers content recovery and confirms settings resets. The library lists sections with summaries, readable sample previews and expanded **Advanced** options. **Variable** now opens a searchable dialog with custom names, fallback text and a personalization preview. Heading numbers are optional. The tablet assistant opens beside your email; phones get preview data, test sends and simulation controls.
### Fewer steps from an audience to a send
Saved segments and lists now offer **Create campaign** and **Create sequence**, with animated guides and a banner keeping your selected audience visible during setup. Sequences trigger when contacts enter the segment or join the list. Shopify settings show tracking and sequence status separately, with one step to review an existing sequence or set one up. The CSV importer remembers your last mapping corrections and import options for explicit reuse after reviewing column differences. Consent, contact source and sequence enrollment are still confirmed on every import.
### Automatically save your first generated email
When you create a campaign or transactional email from a prompt, your first applied result now saves automatically so reopening or refocusing the editor does not restart generation after a successful save.
## Smarter saved AI styles
Saving an AI style now asks follow-up questions. The dialog shows what gets reused and the layout habits it noticed, such as a dotted divider above and below every button or opening with a video, so you can keep or drop each one and add notes. New AI emails follow the kept habits and get those dividers inserted automatically. After a generation, the assistant offers a few follow-up suggestions proposed by the writer for that email, and the **Save as AI style** chip stays hidden while the email already matches your saved style. The API, CLI and MCP accept the same `layoutRuleIds` and `notes`.
## Explicit email restyling
Your visual requests now take priority over saved designs and company color defaults, including when you ask for new copy. Text-only results are retried instead of being presented as a restyle. Reference looks, such as "make it OpenAI style", now return a complete palette and fonts, with one retry when the colors are missing. You can manage your saved AI style from the AI Assistant header.
## Manage saved AI styles from your tools
You can now inspect, save and clear your company's AI email style through the API, CLI and MCP, including unsaved canvas snapshots. Revision checks protect newer styles from stale requests, with the same source permissions and validation as the editor.
### Reuse an email's style for AI
Choose **Save as AI style** from the AI sidebar’s header style button, the suggestion after generation, **Style** settings, or **Campaign settings**. Confirm to make the design your company’s default for future AI emails. Explicit requests for a different look, such as a ChatGPT welcome email, take precedence over your saved style. New generations reuse its fonts, colors, spacing, and matching block styles with fresh copy. You can replace or clear the saved style with confirmation; existing emails stay unchanged. [Learn more](./concepts/email-themes#reuse-an-emails-style-for-ai).
### Protection against repeated transactional junk
Repeated transactional content sent to many recipients can now trigger a junk/list-testing review. Substantiated junk findings reject later matching deliveries without pausing unrelated email. Review continues in the background, and rejected deliveries appear as failed rather than being held for automatic replay. [Read about delivery status and retries](/api-reference/transactional/send#repeated-junk-content-rejection).
### Cart, order, and free-shipping blocks
Store emails can now show each recipient their own basket. The new **Cart items** and **Order summary** blocks render the trigger event's line items with thumbnails, quantities, line totals, a total row, and a checkout or tracking button. The **Free shipping bar** shows how far a cart is from your threshold. Product cards gained a sale badge, a star rating, and a low-stock note. Cart Recovery uses the new blocks, and ten store designs join the gallery, from Price Drop and It's Back to Shipping Cutoff. The designed gallery now shows every design whatever your site type, with the closest fits first. See [Cart and Order Blocks](/concepts/cart-and-order-blocks).
### Accurate counts for older segments
Saved segment filters without internal filter IDs now retain their conditions when evaluated, fixing inflated counts and audiences without requiring you to recreate the segments.
### Start sequence emails from your own templates
Every Send Email step now has a **Start from** picker: your saved templates, the designed template gallery, or any existing email. Whatever you pick is copied into a new email for that step, so one master design can carry a whole sequence while each step keeps its own copy and images. Describe the step and **Generate in this design** rewrites only the text inside the layout; steps that still share one email show a **Shared** chip with a one-click way to detach. Mark any email as a template from the Emails page, the editor header, or with `isTemplate` over the API, CLI, and MCP.
Interface scope: standalone/sequence source-design copying and AI rewriting within the selected
layout are dashboard-only in this release, keeping source, translation, and
fallback-copy review in the interactive authoring flow. REST, CLI, and MCP
support template marking and normal content creation; they do not yet expose
equivalent standalone/sequence source-design operations. Existing campaign copying
through REST or MCP with `templateId` remains available. This intentional parity exception is
documented with the supported alternatives below.
[View documentation](/concepts/email-templates)
### Lifecycle templates and Shop components
The template gallery gains a **Lifecycle** category for stores - brand lookbook, welcome offer, cart recovery, post-purchase, review request, care guide, and win-back - and the editor gains a **Shop** component category with lookbook tiles, category tiles, product spotlight, review strip, perks row, and announcement bar, none of which need a product catalog.
[View documentation](/concepts/email-components#built-in-shop-components)
### Marketer team role
Invite teammates as **Marketer** when they should run campaigns but not the
workspace. Marketers create, edit, schedule, and send campaigns and sequences,
manage subscribers, lists, tags, segments, forms, and landing pages, and read
analytics and the inbox. They never see transactional emails, settings,
billing, integrations, API keys, or the team page, and the same limits apply
when they use a personal API key, the CLI, or MCP. Assign the role from
Settings → Team, the team API, `sequenzy team invite --role marketer`, or the
`invite_team_member` tool.
[View documentation](/api-reference/team/invite)
### Higher API rate limits
The standard API budget is now 300 requests per minute per key (up from 100),
and `POST /api/v1/transactional/send` has its own budget of 1,000 requests per
minute and 50 per second so bursts of confirmation emails no longer compete
with your subscriber and campaign calls.
[View documentation](/api-reference/introduction#rate-limiting)
### Accounts for B2B products
Model the companies, workspaces and teams your contacts belong to. Accounts
carry plan, seats, MRR and trial dates that fan out to every member for
segments and `{{account.*}}` merge tags, members have roles, and account
events such as `trial_ending` reach the owners (or admins, or everyone) as
normal contact events so sequences run for the right people. Matching-field
enrollment on `account.externalId` allows one active sequence run per company.
If your events already carry an organization id, Sequenzy detects it and
backfills accounts in one click. Available in the dashboard, API, CLI, MCP
and SDKs. [View documentation](/concepts/accounts)
Contact events can attach accounts through the CLI and MCP, and account language attributes now select the same localization in campaigns, previews, and test sends.
### Dedicated OpenAI-reviewed MCP surface
ChatGPT and the OpenAI plugin directory now use the dedicated
`https://api.sequenzy.com/v1/mcp/openai` surface. It shares Sequenzy's MCP
implementation while omitting secret-producing integration, API-key, webhook,
raw-delivery, and inbound-secret rotation operations. Open-ended contact,
event, form, webhook-sample, personalization, and feedback inputs reject
restricted personal-data fields; responses remove restricted fields, internal
diagnostics, and unnecessary account or credential identifiers. Restricted
merge-tag selectors and third-party credential-bearing URLs are blocked, and
email previews on this surface use sample or policy-checked inline subscriber
data instead of loading an uninspected stored subscriber by ID.
The standard remote endpoints (`/mcp` and `/v1/mcp`) and local stdio package
retain the full MCP feature set for trusted clients, including their existing
credential-based integration setup and one-time API-key and webhook-secret
flows. The OpenAI-reviewed surface omits raw nested API error payloads while the
standard surface retains its existing debugging detail. Product feedback is
sent only when the user explicitly asks.
[View MCP data and privacy guidance](/concepts/mcp-permissions#data-and-privacy)
### Delete obsolete sender profiles safely
Sender (From) profiles can now be permanently deleted through the REST API,
CLI, and MCP. Sequenzy blocks deletion while a live sending surface uses the
profile, protects the last sender, and reports the fallback chosen for eligible
drafts and account defaults. [View documentation](/api-reference/sender-profiles/delete)
### A full Sequenzy app for Make
The Make custom app now covers the platform rather than just subscribers. Eight instant triggers start scenarios from subscriber, email, reply, poll, SMS, campaign, sequence, and import events, and new modules handle lists, segments, bulk tags, imports, sequence enrollment, campaign scheduling and metrics, transactional sends, deliverability suppressions, and commerce orders and checkouts. Lists, tags, segments, sequences, campaigns, and templates now load as live dropdowns instead of pasted IDs.
[View documentation](/integrations/make)
### Start n8n workflows from Sequenzy events
The Sequenzy n8n community package now includes a trigger node that starts workflows from selected email, SMS, campaign, subscriber, and sequence events. It creates and manages the signed outbound webhook for you, and the opt-in `campaign.sent` event provides one completion signal after an email or SMS campaign settles.
[View documentation](/concepts/events)
The Monday weekly report now goes only to the workspace owner by default. Teammates who want it can turn it on for themselves under **Settings -> Notifications**, via `sequenzy notifications set --weekly-report weekly`, or with the `update_notification_preferences` MCP tool.
Tag pickers and per-tag subscriber counts now stay responsive in workspaces with tens of thousands of tags.
### Weekly report every Monday
Each workspace that sent more than 10 emails last week now gets one short Monday email: one row of numbers against the week before and the sequences that slipped, each linked so the fix is one click away. The report is on by default and per person; turn it off under **Settings -> Notifications**, `sequenzy notifications set --weekly-report off`, or the `update_notification_preferences` MCP tool. [View documentation](/concepts/account-notifications#the-weekly-report)
Video blocks now accept a custom thumbnail everywhere: pick an image in the editor, or set `thumbnailUrl` in API, CLI, and MCP block writes. Omit the field to fall back to YouTube's own still while keeping `videoUrl` as the click destination.
Hosted unsubscribe pages and embedded subscription preferences now show public list names without exposing list descriptions. Descriptions remain internal workspace metadata.
Hosted unsubscribe pages now lead with a single **Unsubscribe** action that
clearly stops marketing email, then keep granular preferences available below.
The success state stays visible with an undo option, and free-tier attribution
has been reduced to quiet footer text.
### Video tutorials inside the app
The dashboard now has a Tutorials section in the sidebar: short video walkthroughs of everything from your first campaign to sequences, deliverability, and agentic setups, grouped into a guided track, deep dives, and quick answers. Search understands synonyms - typing "automation", "workflow", or "flow" surfaces the sequences video - and each video is marked watched as you go. New workspaces also get a link to the tutorials during onboarding.
Emails generated through the API, CLI, and MCP now follow your designed/plain email style setting the same way the dashboard does - designed by default unless you chose plain text - and the `style` parameter accepts `designed` or `plain` to force a specific style per request.
Each company can now send up to 100 test SMS messages in a rolling 24-hour window, replacing the previous five-per-hour cap. Test sends still require a verified number, charge SMS credits, bypass quiet hours, and stay out of campaign and automation statistics. [View documentation](/concepts/sms#test-sends)
### Map Attio lists without reconnecting
A connected Attio integration with no list mappings used to be dashboard-only to finish. Agents can now read live Attio people-lists and save `listMap` with the stored token. No access token re-paste.
* [`GET /api/v1/integrations/{id}/attio`](/api-reference/integrations/attio-get) returns Sequenzy lists, live Attio people-lists, and the saved map
* [`PATCH /api/v1/integrations/{id}/attio`](/api-reference/integrations/attio-update) saves mappings (`{}` clears them) and/or company matching
* CLI: `sequenzy integrations attio ` and `sequenzy integrations set-attio-lists --map list_abc=field-guide`
* MCP: `get_attio_mapping` and `update_attio_settings`
* `get_integration` now recommends mapping when `mappedListCount` is 0, and `availableActions` includes `update_attio_settings`
[View documentation](/integrations/attio)
### Landing page reports like Mailchimp
Published landing pages now record visits, unique visits, clicks, and subscribes, with a conversion rate, daily chart, referrers, and UTM sources. Known crawlers are excluded from the main totals and listed separately as crawler hits, so a founder can tell whether 31 views were people, Googlebot, or the team opening the public URL. Preview URLs and the editor still do not count. Open **Stats** from a page's list row, or use `get_landing_page_stats` / `GET /api/v1/landing-pages/{id}/stats` / `sequenzy landing-pages stats`. [View documentation](/concepts/landing-pages#metrics)
### Enrollment history shows why a branch took else
Completed sequence tokens now keep a redacted record of how they entered and
which if/else they took: `enteredVia`, `entryContext` (event keys, never
values), and `branchDecisions` (compared field name plus
missing/empty/nonempty/equals\_expected). New enrollments persist that decision
on the token. For older completed tokens, `GET
/api/v1/sequences/{id}/enrollments/{enrollmentId}`, `get_sequence_enrollment`,
and `sequenzy sequences enrollment` reconstruct the graph walk from ClickHouse
and redact any raw compared values that were stored in node-completion
metadata. [API docs](/api-reference/sequences/get-enrollment) · [MCP
docs](/concepts/mcp-tools#sequences) · [CLI docs](/concepts/cli#sequences)
### Agents can explain and resolve Sequenzy email branding
`get_company`, `GET /api/v1/companies/{companyId}`, and `sequenzy companies get` now report the effective "Sent with Sequenzy" entitlement: whether the badge is visible, whether the current owner subscription removes it, the plan/status reason, and the required upgrade or renewal action. They also return the real **Account -> Subscription** URL; `get_app_urls` and `sequenzy urls --settings-tab billing` now resolve there instead of constructing a nonexistent billing Settings tab. Branding remains a server-enforced plan entitlement rather than a writable footer toggle, and entitlement changes automatically apply to future sends from existing live sequences. [API docs](/api-reference/companies/get) · [CLI docs](/concepts/cli#companies) · [MCP docs](/concepts/mcp-tools#company)
### Block field lookup survives a partial MCP grant
Agents authoring email blocks are now told to enable `get_email_block_schema` on the Sequenzy connector when it is missing from the tool list, and pointed at the same reference on `sequenzy://email-blocks`, `GET /api/v1/email-blocks`, and `sequenzy blocks ` instead of inferring field names from live HTML. Body copy color is documented as `styles.textColor` (or the top-level `textColor` alias), not an inline `style` on `
` or ``. [View documentation](/concepts/mcp-tools#email-blocks)
### Manage toll-free numbers like sending domains
Every SMS number now has its own detail page: status, the toll-free verification data you submitted, per-number usage (sends, delivered, failed, credits, last sent), and a label or brand-prefix override, all in one place. You can also remove a number you no longer need - freeing one of your workspace's number slots, now raised to 100 so multi-brand senders can register a number per brand - and choose exactly which number a test send goes out from. [View documentation](/concepts/sms)
### One design system per brand, coherent sequences
Every AI-generated email now renders inside a single per-company design system: one composition style, one visual grammar (kickers, alignment, buttons, dividers, density, openers), and one theme - instead of each sequence email re-rolling its own look. Sequence steps no longer drift to different colors, fonts, or design languages mid-sequence. The identity lives in your design direction text (Settings), starts derived from your brand, and is adjustable via the API (`/api/v1/email-design-system`), the CLI (`sequenzy email-design-system`), and MCP (`get_email_design_system` / `update_email_design_system`).
[API docs](/api-reference/email-design-system/get) · [CLI docs](/concepts/cli) · [MCP docs](/concepts/mcp)
Brand onboarding also got sharper: enrichment now retries transient failures instead of silently giving up, and your email theme's mood follows the brand's actual tone of voice.
### Native Attio destination for list joins
Gated lead magnets and other list-gated content can push new subscribers into Attio without Zapier. Connect with an Attio access token, map a Sequenzy list to an Attio list, and each `subscriber.list_subscribed` upserts the person by email (optional company from the work domain) and adds them to that Attio list. Outbound only: no inbound webhook, no backfill, add-only. [View documentation](/integrations/attio)
### Safer agent access can edit sequence A/B variant copy
**Safer agent access** now includes `ab_tests:write`, so an agent can rewrite cart and browse abandonment variant bodies without bumping to AI drafting. Live tests still need `confirmLiveChange`. Rejected writes through `update_template`, `update_sequence_node`, or the sequence `emails[]` array now name `update_ab_test_variant`, include the test and variant IDs when known, and say to enable that tool on the connector rather than retrying another email tool. `GET /api/v1/templates/{id}` also surfaces that pointer when the email belongs to a variant. Existing Safer agent access keys need the preset re-applied (or `ab_tests:write` added) to pick up the new permission. [View documentation](/concepts/mcp-tools#editing-the-content-of-a-sequence-ab-step)
### Sequence A/B variant copy on get\_sequence
`GET /api/v1/sequences/{id}` now returns each A/B variant's full `blocks` on `emails[].abTest.variants[]` when the key has `ab_tests:read`, so a sequence audit (cart or browse abandonment included) can read competing CTA, promo, and review copy without a second `get_ab_test` call. Step-level `blocks` are still control variant A only. [View documentation](/api-reference/sequences/get)
### Segment by marketing vs transactional email activity
Engagement filters can now be scoped to an email type: `marketing:` matches marketing-policy sends from campaigns, automations, and the Send API, while `transactional:` matches transactional-policy sends. That lets you build guardrail segments like "no marketing email in the last 7 days" without receipts or password resets counting against the rule. Policy scopes use the snapshot captured with each send; ambiguous older automation and Send API events without one remain available through unscoped engagement filters. The scoped values work in the dashboard segment builder, the API, the CLI, and the MCP tools. [View documentation](/concepts/subscribers)
### Preview a landing page before publishing it
Draft landing pages now include a signed `previewUrl` on create, get, list, and
update, and a `POST /api/v1/landing-pages/{landingPageId}/render` endpoint plus
the `render_landing_page` MCP tool. The preview shows the visitor-facing page
without putting it on your public domain, is not indexed, and does not collect
form signups until you publish. [View documentation](/api-reference/landing-pages/render)
### Sequence simulation
You can now dry-run a sequence before turning it on. `GET /api/v1/sequences/{id}/simulate`, `sequenzy sequences simulate`, and the `simulate_sequence` MCP tool report who currently matches and activation blockers - without sending mail or enrolling anyone. Activating does not auto-enroll existing matches. Pass a stored subscriber to walk their branch path, for example to confirm a paid user would not get an upgrade email. [View documentation](/api-reference/sequences/simulate)
### MCP specification 2026-07-28
The hosted Sequenzy MCP server at `https://api.sequenzy.com/v1/mcp` now speaks the [2026-07-28 MCP specification](https://modelcontextprotocol.io/specification/2026-07-28) while remaining compatible with 2025-era clients. Modern clients discover the server per request, send `Mcp-Method`/`Mcp-Name` headers, and receive cacheable list results; existing Claude, Codex, and other 2025 clients keep working through the same URL. OAuth now advertises Client ID Metadata Documents, includes the RFC 9207 `iss` parameter on authorization redirects, and accepts `application_type` during dynamic client registration. [View documentation](/concepts/mcp)
### Image shapes
Mask any image with a fixed URL into an arch, circle, leaf, petal, squircle, scallop, flower, or blob straight from the image block's Style settings - external images are saved into your media library automatically when you apply a shape. The shape is baked into the delivered image on our servers, so it renders identically in every email client - Gmail and Outlook included - and matches the editor preview exactly. [View documentation](/concepts/visual-blocks#image-shapes)
### Group email content into responsive layouts
The email editor can now group sibling blocks into a stack, equal-width row,
grid, or image overlay without rebuilding the content. Groups keep their
layout in preview and sent email HTML, can be nested up to three levels, and
can be moved, duplicated, ungrouped, or reorganized with keyboard and drag
controls. The same recursive group structure is documented for the API, CLI,
and MCP so existing grouped emails remain readable across every surface.
### Sequence AI assistant can no longer discard your emails
The sequence AI assistant now refuses to delete existing email steps unless you explicitly ask for them to be removed or rebuilt - asking it to reorder, connect, or space out your emails keeps every email exactly as you wrote it and only adds delays or updates around them. Before applying AI changes, the approval box also lists each high-impact change (like deletions) by name and states clearly that approving saves immediately to your live sequence.
### Default lists apply to new contacts, not every write
Your Default Lists setting now decides where a contact **created** by an untargeted write lands without changing memberships during ordinary profile or attribute upserts. Passing `lists` explicitly still works as before, while capture submissions and explicit reactivation events can reapply target or default lists so returning contacts actually resubscribe. [View documentation](/concepts/subscribers)
`GET /v1/account`, `sequenzy account`, and the MCP `get_account` tool now report whether the current API key can actually deliver mail (`canSendLive`) and which delivery permissions it lacks (`missingLiveDeliveryScopes`), so an agent holding drafting-only permissions like `transactional:write` finds out before writing an email it cannot send. [View documentation](/concepts/mcp)
### Abuse protection for public signup forms
Public form and popup endpoints now enforce daily per-IP limits alongside the existing per-minute burst limit, so slow scripted signups that stayed under a one-minute window are caught. A privacy-preserving cross-workspace recipient limit also caps how often a single address can be signed up through forms, popups, and landing pages, which contains subscription bombing even when an attacker rotates IPs and signup surfaces. Every non-restricted workspace member is alerted by email when a workspace's public signup volume spikes far above its usual baseline. [View documentation](/concepts/account-notifications)
### Trial lifecycle events for Polar
Polar trials are now tracked as trials instead of purchases: a trial signup fires `saas.trial_started`, converting fires `saas.trial_ended`, cancelling during the trial fires `saas.trial_cancelled`, and Sequenzy schedules `saas.trial_will_end` about 3 days before the trial ends (Polar has no native reminder webhook - the reminder is cancelled automatically if the trial converts or is cancelled first). The $0 trial-start order no longer records a purchase, duplicate purchase/cancel events from Polar's webhook fan-out are collapsed, and abandoned Polar checkouts now fire `saas.checkout_expired` (existing Polar connections: add `checkout.expired` and `subscription.uncanceled` to your webhook's selected events in the Polar dashboard). Stripe got the same trial-accuracy fixes for $0 invoices and checkouts. Payment webhooks now also apply the same status tags (`trial`, `customer`, `cancelled`, `churned`, billing-interval tags) that a manual Sync Revenue run applies, so webhook-created contacts are tagged consistently. [View documentation](/integrations/polar)
### Branded double opt-in confirmation
The hosted confirmation page that double opt-in links open now shows your company name, logo, and brand color instead of the Sequenzy site chrome, and you can set where subscribers land after confirming - a thank-you or welcome page on your own site - from Settings, the [API](/api-reference/tracking-settings/update), the CLI, or MCP. Without a redirect, confirmed subscribers stay on your branded confirmation page; nothing sends them to sequenzy.com anymore. [View documentation](/concepts/double-opt-in)
### Python, PHP, and Go SDKs
Official Sequenzy SDKs are now available in Python (`pip install sequenzy`), PHP (`composer require sequenzy/sequenzy`), and Go (`go get github.com/sequenzy/sequenzy-go`), alongside the existing TypeScript SDK. All four are generated from the same OpenAPI spec, so every endpoint and field in the [API reference](/api-reference/introduction) is available in each language. The TypeScript SDK's v2 release keeps the same import and constructor (`new Sequenzy({ apiKey })`) and the same resource layout (`client.subscribers.create`), but some method names and signatures changed from v1 - review your call sites when upgrading.
### Send from several brands under one business
If you trade under more than one name, each toll-free number can now be
registered with its own brand: its own DBA name, website, verification contact,
consent flow, and use case. Your legal business details - registered name, EIN,
address - are stored once and reused by every number, so nothing is retyped.
Carriers register each number as its own messaging program and check that the
brand people opted in to matches, which is why a second brand needs its own
number rather than a second copy of the first one's application.
[View documentation](/concepts/sms)
### Verification is per number, with its own page
Each number now shows its own status and its own carrier feedback in
`Settings -> SMS`, and **Verify this number** opens a page for just that
number. A number waiting on the carrier no longer blocks adding another, so a
new brand can start its registration while an earlier one is still in review.
The **Business name** field is for your registered legal entity only - a DBA
there is the most common cause of "Business Name Must Match Official Records".
[View documentation](/concepts/sms)
The quick filters above the subscriber table now adapt to your workspace: revenue, trial, order, and cart filters only appear when the matching tags and attributes actually exist in your data, so you see shortcuts you can use instead of empty ones.
Asking the email AI assistant to add content - "add more information from our changelog" - now keeps the email you wrote and extends the matching section instead of rewriting the email or bolting on a duplicate section, and requests are understood in any language and phrasing (including timeframes like "last 1 month") when pulling changelog updates.
### Send as a non-default brand from MCP, CLI, and the Send API
`POST /api/v1/transactional/send`, the `send_email` MCP tool, and `sequenzy send` now accept optional `senderProfileId`/`fromEmail`/`fromName` and `replyProfileId`/`replyTo`/`replyToName` so a one-off send can use an existing verified brand identity instead of the company default. Multi-brand accounts no longer need a throwaway draft campaign to test a non-default From. Pass `fromEmail` (and `fromName` if that address has several identities) without looking up profile IDs. These fields look up existing send-ready identities and do not create new profiles.
[API docs](/api-reference/transactional/send) · [MCP docs](/concepts/mcp) · [CLI docs](/concepts/cli)
### Audience size is on stats and lists
`GET /api/v1/metrics` (and the `get_stats` MCP tool) now returns live `subscriberCount` and `activeSubscriberCount` next to send metrics, so you do not have to page `search_subscribers` to learn how big the audience is. `GET /api/v1/lists` (and `list_lists`) includes the same two fields on every list.
[API docs](/api-reference/analytics/metrics) · [MCP docs](/concepts/mcp)
### Use your own icons in feature blocks
Feature blocks are no longer limited to emojis: upload an image or paste an icon URL - an exported Lucide icon, a Flaticon PNG, anything on a public URL - and it replaces the emoji for that item. One **Icon size** setting keeps every icon in the block the same size, and the icons render identically in the editor, the preview, and the sent email.
[View documentation](/concepts/visual-blocks)
### Sequences can start from several lists or tags
A `contact_added` trigger can now name more than one list, and a `tag_added` trigger more than one tag - so "enter this sequence when someone joins Newsletter **or** Trial" no longer needs two duplicate sequences or a throwaway segment. Pick several values in the trigger step, or send `listIds` / `tagNames` through the API, CLI, and MCP. A contact still enters once no matter how many values they match. The automatic stop condition follows the value that enrolled each contact: leaving Newsletter stops a Newsletter enrollment, while leaving Trial stops a Trial enrollment.
[View documentation](/concepts/sequences)
### Every enrollment says what caused it
The sequence subscriber list now shows **Entered via** on each contact - the list, tag, segment, or event that enrolled them, or "Enrolled manually" for a dashboard or API enrollment. The same attribution is available as `enteredVia` on the enrollments API, in `sequenzy sequences enrollments` (including CSV exports), and through MCP. It answers the first question a multi-list or multi-tag trigger raises: which one actually brought this person in.
[View documentation](/api-reference/sequences/list-enrollments)
### API, CLI, and MCP renders say which conditions they could not check
A conditional block that splits on a tag, segment, or other stored subscriber state is evaluated per recipient at send time, so a render preview had nothing to evaluate it against and quietly returned the else branch - identical to what a contact who genuinely does not match would see. The render endpoints, `sequenzy campaigns render` and its siblings, and the `render_email` MCP tool now return every condition they could not evaluate as `unevaluatedConditions`, and can evaluate tag conditions for an ad-hoc contact that lists its own `tags` (`--tag` on the CLI). The block reference gained `conditionFields`: which operators each condition field accepts, how its value is shaped, and what a preview needs to check it. Dashboard previews are unchanged.
[CLI docs](/concepts/cli) · [MCP docs](/concepts/mcp)
### Test emails tell you what actually happened
Sending a test used to report success the moment the job was queued, before anything had been attempted - so a test that was suppressed, hit a blocked sending domain, or failed at the provider still showed a green toast and never arrived. The confirmation now says the send is scheduled, and any test send that fails, bounces, or is suppressed appears in Sent emails marked as a test, with the reason. Successful tests stay out of the list.
[View documentation](/api-reference/email-sends/list)
### See exactly who your emails skip, and keep bounce blocks in your workspace
Subscribers has a new Suppressed tab listing every associated recipient you cannot reach, with the reason, when it happened, and whether the block is platform-wide or specific to your workspace. Only SMTP evidence that conclusively identifies an invalid destination creates a global block. Ambiguous hard bounces remain protected inside the sending workspace, spam complaints are protected, and workspace soft-bounce escalations have one-click removal. Repeated delivery failures now suppress an address for your workspace only instead of everywhere, so one recipient's spam filter can no longer block that person for other senders. The same list is available through the API, `sequenzy suppressions list`, and MCP.
[View documentation](/api-reference/suppressions/list)
### Remote MCP connections stay up in third-party clients
Clients that open the optional event-stream request on `https://api.sequenzy.com/v1/mcp` were handed a stream that closed immediately, which many of them read as the server dropping the connection and answered by reconnecting every second. The endpoint now declines that request the way the protocol expects, so those clients connect once and stay connected. The remote MCP endpoint also gets its own request budget of 300 per minute instead of sharing your REST API limit, so a busy agent session no longer throttles itself into a dropped connection.
[View documentation](/concepts/mcp)
`select_company` now persists for the life of a local MCP session, and `get_account` recovers cleanly when the selected company is later deleted or becomes inaccessible.
[View documentation](/concepts/mcp)
### Test sends now tell you when a recipient is suppressed
Test emails to a suppressed address used to report success while nothing arrived, which read as "email is broken". The test-email dialog now warns up front which selected recipients are suppressed and why, and the send result names them instead of reporting plain success.
### A/B test steps are visible and editable from the sequence surface
A sequence step converted to an A/B test kept its copy on the test's variants, but nothing in the sequence surface said so: the step disappeared from `sequence.emails`, `emailCount` read `0` for a sequence made entirely of A/B steps, and rejected edits gave no hint where the content had gone. `GET /api/v1/sequences/{id}` now lists A/B steps with their variants and a pointer at the endpoints that edit them, `sequenzy sequences get` prints each variant, and update attempts on the node redirect to `update_ab_test_variant` instead of failing as an unknown field.
[View documentation](/api-reference/sequences/get)
The block field reference now expands nested objects, so `get_email_block_schema`, `sequenzy blocks `, and `GET /api/v1/email-blocks` describe a repeat block's `productSource` in full - including the `strategy` field that picks the best-sellers, newest, or recently-viewed ranking - instead of reporting it as an opaque object. Rejected writes and dropped-field warnings name the same nested fields.
[View documentation](/concepts/product-recommendations)
Sequence email positions shown by `sequenzy sequences get`, MCP, the sequence AI chat, and the editor's step badges are now derived from the sequence's actual structure instead of stored ordinals that can go stale after edits. `GET /api/v1/sequences/{id}` exposes this value as `structuralStepNumber` while retaining the legacy `stepNumber` field for compatibility. Emails in parallel branches intentionally share a structural depth; after unequal branches merge, numbering continues from the longer incoming path.
### Built for data pipelines: bulk events, idempotent imports, completion webhooks
Syncing from a warehouse or DAG into Sequenzy no longer needs one request per contact. The new `POST /api/v1/subscribers/events/imports` endpoint records bounded batches of up to 25 events for many subscribers in one call, with a required source-owned event ID so retries preserve one receipt and repair downstream work idempotently. Subscriber imports accept an `idempotencyKey`, and `subscriber_import.completed` publishes final reconciled counts from a durable retry intent. All three are available from the API, CLI (`sequenzy subscribers events-import`, `--idempotency-key`), and MCP.
[View documentation](/guides/data-pipeline-ingestion)
### Subscriber imports account for every skipped row
A finished import now explains every row it excluded: the skipped-reason counts add up to the skipped total. Previously an import processed in several chunks could report more skipped rows than reasons, because chunks completing at the same time overwrote each other's reason counts. Rows dropped for having no usable email or phone are now reported as `missing_identity` instead of disappearing from the totals, and `sequenzy subscribers import-status` prints the reasons.
[View documentation](/concepts/subscribers)
### Cc, Bcc, and multiple recipients from the CLI and MCP
The transactional send API has always accepted a shared recipient list, but `sequenzy send` and the `send_email` MCP tool took a single address, so agents had to fall back to raw HTTP or send one copy per person - which never produces a real Cc. Both now accept up to 50 `to` addresses plus `cc` and `bcc` lists on a transactional send, so one email reaches everyone with the recipient list they expect. Marketing sends are unchanged: one recipient, no Cc or Bcc.
[View documentation](/concepts/cli)
### Landing pages on root domains
Landing pages can now live on a root domain like `example.com`, not just a subdomain. Connect the root domain from the dashboard, API, CLI, or MCP and point an A record at `76.76.21.21` - the dashboard shows the exact record to add. If `www` has a CNAME record pointing to `pages.sequenzydns.com`, it redirects to the root automatically. Subdomain setups are unchanged.
[View documentation](/concepts/landing-pages)
The API, CLI, and MCP can now restyle a single sequence email. Email steps accept an `emailTheme` patch that overrides just that email's colors, typography, and layout - so making one draft's background white no longer means changing the account-wide theme. The patch merges into the email's current theme, `null` clears the override, and `GET /api/v1/sequences/{id}` reports the stored override per step.
[View documentation](/api-reference/sequences/update)
### Contact-added sequences can wait for a list, or not
The Contact added trigger now says which of the two it means. **Any contact** - the default, and what every existing sequence already did - starts the sequence for every contact added, including the list-less contacts that integrations such as PostHog, Stripe, or Supabase create when no list targeting is set. **Any list** is new and waits until the contact actually joins a list. Enroll Existing Subscribers follows the same rule, so backfilling an Any contact sequence no longer skips contacts who are in no list. Available from the dashboard, API, CLI (`--list-scope`), and MCP (`listScope`).
[View documentation](/concepts/sequences)
Duplicating an email step in a sequence no longer reports "AI is writing this email" with a button that does nothing. The copied step now shows the subject of the email it is copying, says its copy is created when you save the sequence, and offers to swap in an existing template if you change your mind.
A duplicated email step also takes the next step number instead of reusing its source's, so `GET /api/v1/sequences/{id}`, `sequenzy sequences get`, MCP, and the AI chat no longer show two different emails as the same step.
A segment built as a list of specific email addresses now opens quickly on large audiences. Naming dozens of addresses in one segment could previously take long enough to time out and return an error when the segment was listed in the dashboard or through `GET /api/v1/subscribers`.
Agents on the hosted MCP server no longer insist you reauthorize an hour after you connect. The credential never expired, but we advertised an expiry for it, so clients such as Claude reported the connection as unauthorized and asked you to authorize again while every tool call kept succeeding.
The toll-free SMS verification form now remembers what you last submitted to the carrier, so if the carrier asks for changes you can fix a single field and resubmit from any browser, device, or teammate's account instead of retyping the whole form.
### Video blocks on landing pages
Landing pages now have a Video element: paste a YouTube link and Sequenzy builds the right embed, so embed code no longer has to go in a Custom HTML block. Pick a 16:9, 4:3, 1:1, or 9:16 shape and a width. YouTube videos load from youtube-nocookie.com, so no tracking cookies are set until someone presses play. Other providers and direct video files are not supported. Custom HTML blocks also now render in the editor's Preview mode instead of showing a placeholder.
[View documentation](/concepts/landing-pages)
### Content above the hero on landing pages
Landing pages can now carry a full-width band above the hero for an announcement bar, banner image, or logo strip. Drag a section onto the top half of the hero to place it there, or move any section up past the hero - and the hero's move-down control now lifts the section beneath it above the hero. The hero keeps its own layout, including the form card beside it. In page content these blocks use the new `top` slot, so the API, CLI, and MCP can set them too.
[View documentation](/concepts/landing-pages)
### Anchor CTAs and scroll animation for landing pages
Landing page buttons and pricing CTAs now accept in-page anchors, so a long page can repeat its call to action and scroll back to its single signup form with `#form` instead of sending visitors somewhere else. Anchor CTAs stay in the same tab and scroll smoothly, and `#section-`, `#block-`, and `#top` work the same way. The Style panel also gained a scroll animation setting - fade, slide up, or zoom in at three speeds - that plays as each section enters the viewport on the published page and is skipped for visitors who prefer reduced motion. Both settings work from the dashboard, API, CLI, and MCP.
[View documentation](/concepts/landing-pages)
### Read and set your workspace default lists from the API
The Default Lists setting - which lists a contact joins when a form, API call, event, import, or integration creates it without naming one - is now readable and writable outside the dashboard, as `defaultSubscriberListIds` on `get_company` / `update_company`, `sequenzy companies update --default-list`, and the company API. This matters most before connecting a store: an integration without its own list targeting, such as WooCommerce, sends its whole customer backfill to whatever this points at. In the dashboard the setting lives under Contacts -> Lists -> Default Lists.
[View documentation](/concepts/subscribers#default-lists)
### Merge tags with a fallback no longer hide typos
A merge tag whose name nothing provides used to disappear from preview diagnostics as soon as it had a fallback, so `{{ subscriber.frstName | default: "there" }}` greeted an entire list as "there" - stored first names included - while the rendered email looked perfectly personalized. Previews now report those under `unresolvedMergeTags` with reason `unknown`, and stay quiet when the fallback is simply covering a contact who has no first name. `{{subscriber.firstName}}`, `.lastName`, `.email`, and `.name` also resolve now, so both the prefixed and unprefixed forms work.
[View documentation](/concepts/campaigns#personalization)
The per-workspace sending cap is now 300,000 accepted emails in a rolling 24-hour window, up from 200,000, so high-volume campaigns no longer have to spill into the next day.
Connecting a landing page domain now rejects root domains like `example.com` up front - with a pointer to use a subdomain such as `www` or `pages` - instead of accepting them and then asking for a CNAME record that DNS cannot create at a domain root.
### Group form and popup blocks into responsive layouts
Forms and popups can now group related blocks into stacks, equal-width rows,
grids, or image overlays directly on the builder canvas. An overlay uses one
direct image as its background and supports tint color, intensity, and content
position. Its gap setting spaces the foreground blocks without affecting the
background image; nested images only enable Overlay for their own subgroup.
Groups can be nested up to three levels, dragged as a unit, and moved into or
out of other groups. The same recursive group structure is supported by the
API, CLI, and MCP, and hosted forms, native snippets, and popup embeds continue
to discover nested fields for validation and submission.
[View documentation](/api-reference/widgets/update-saved-form#layout-groups)
### Change an integration's target lists from the API
Choosing which lists an integration's new contacts join is no longer dashboard-only - it is now on the API, CLI, and MCP too. Useful when an auth or ecommerce integration is adding every new signup to lists it should not: point it at one list, at none, or back at your workspace defaults. New targeting applies from the integration's next write onward and never removes anyone from a list, and pausing bulk sync remains a separate setting that does not stop the provider's live webhook. Use `PATCH /api/v1/integrations/{id}`, `sequenzy integrations set-lists`, or the `set_integration_list_targeting` MCP tool; `integrations get` now reports the current targeting with list names.
[View documentation](/api-reference/integrations/update)
HTML blocks in the email editor are now easier to work with: selected blocks open in a visual editor (the same one used for imported HTML emails) so you can edit text, links, and formatting in place, with a Code toggle for raw HTML and a Paste HTML button that replaces the block's content straight from your clipboard.
### See whether a stop condition has already matched
Enrollment listings now show the sequence's configured stop condition, including its match filters or comparison, and can report whether it matches a contact right now. A stop condition is re-checked when an enrollment next runs a step rather than when its event arrives, so a contact whose stop event has landed can keep reporting "waiting" until the delay expires. Pass `--stop-condition-match` in the CLI, `stopConditionMatch=true` on the API, or `stopConditionMatch: true` to the `list_sequence_enrollments` MCP tool to inspect the current state. The result is a non-atomic snapshot, so a step already past its stop check may still finish.
[View documentation](/concepts/sequences#when-a-stop-condition-actually-cancels)
Campaigns you imported after they were already sent elsewhere are now labelled "Imported" in the campaign list and on the campaign page, where the empty analytics explain that the delivery data stayed with the platform that sent them. Resending to non-openers is unavailable for those campaigns, since Sequenzy has no opens to resend against.
The migration prompt on your home screen now remembers which platform you are coming from, instead of re-deriving it from your DNS records on every visit. The first detection is kept, and picking a provider in the migration flow overrides it for good - so a slow DNS lookup no longer hides the prompt, and platforms that leave no DNS trace can still be named once and remembered.
### Segment integration
You can now stream your Segment events into Sequenzy. Add a Webhooks (Actions) destination pointing at your Sequenzy webhook URL and track calls arrive as `product.*` events that trigger automations and power segments, while identify calls keep subscriber names and attributes in sync. It is one-way, works with your existing instrumentation, and connects from the dashboard, API, CLI (`sequenzy integrations connect segment`), or MCP. If you use Segment Unify, connecting can also backfill your existing contacts' recent event history (the last 14 days Segment's Profile API serves) so event-based segments start with real activity - historical events never start automations or send emails.
[View documentation](/integrations/segment)
### On-site tracking for any website
Browse abandonment and cart recovery no longer need a Shopify store. Create a
publishable key in Settings → Integrations → Website tracking, paste the script
tag into your site, and product views, cart activity, collection views, and
searches start flowing into the same automations. Visitors who browse before
signing in are not lost: their activity is held in the browser and replayed
against their contact after your backend mints a short-lived identity token and
you call `sequenzy.identify(email, identityToken)`. Keys are
restricted to your own origins and manageable from the API, CLI, and MCP.
Developers can also install the browser client from npm as
`@sequenzy/web-sdk`.
[View documentation](/concepts/web-tracking)
A sequence step's Reply-To address now follows the reply profile it points at. Steps used to store a copy of the address made when the step was saved, so editing a reply profile's name or address left already-built sequences replying to the old mailbox even though the editor showed the new one. Editing a reply profile now updates every sequence step using it, and a profile still in use by a step can no longer be deleted.
[View documentation](/concepts/sender-routing#several-display-names-on-one-address)
### Send campaigns in each recipient's timezone
Scheduled campaigns can now be delivered at the same wall-clock time in every recipient's own timezone - schedule for 8pm and each contact receives it when their local clock reaches 8pm. Store a contact's IANA timezone via the new `timezone` field (API, CSV import, CLI, MCP, or the contact page), then pick "Recipient's local time" on the schedule step; the wizard shows how many recipients have a known timezone, and everyone else receives the campaign at your scheduled time. Also available as `sendInRecipientTimezone` on the schedule API, `--in-recipient-timezone` in the CLI, and the `schedule_campaign` MCP tool.
[View documentation](/concepts/campaigns#recipients-local-time)
Bot filtering now catches security appliances that detonate short emails. An email with only three links - a button, a social icon, and a privacy link - previously slipped past the scanner check, because catching a sweep required four links being fetched at once; three distinct links fetched in the same instant now count as machine activity, so Microsoft Defender Safe Links prefetch no longer inflates click rates on small emails. Strict bot filtering additionally recognizes Exchange Online Protection as a scanner source, while leaving the ranges people actually read mail from alone.
[View documentation](/concepts/metrics)
### A field reference for email blocks
You can now look up exactly what each email block type accepts instead of
inferring it from examples: required and optional fields, the allowed values of
every enum field, and the shape of nested item arrays. Available as
`GET /api/v1/email-blocks`, `sequenzy blocks`, and the `get_email_block_schema`
MCP tool. Rejected block writes now name the block type they were validated
against and its fields, so a mistake is usually fixable without a second
lookup - most usefully for numbered lists, which are their own `list` or `steps`
block rather than a variant of a text block.
[View documentation](/concepts/visual-blocks#looking-up-a-blocks-fields)
### One design direction for all your emails
All AI-generated emails for a company now follow one brand-derived visual identity - kicker style, title alignment, button shape, dividers, spacing, and how emails open - across campaigns, sequences, and transactionals, so everything looks like it comes from one brand while each email still varies in content and composition. The first email generation writes this direction into the Design Direction field in Company Settings as plain text: edit it to change how your emails look, or clear it and the next generation fills it in again.
[View documentation](/concepts/email-themes#design-prompt)
### Event retries no longer double-count
`eventId` is now an idempotency key for live events, not just history imports.
Send the same ID twice for a contact and event name and the repeat records
nothing, enrolls nobody, and comes back with `duplicate: true` - so a retrying
workflow tool or redelivered queue job stops inflating "purchased 3 times"
conditions and revenue totals. It is best-effort for events sent within about a
second of each other, so keep your own ledger if you need a strict guarantee.
[View documentation](/api-reference/subscribers/events/trigger#retrying-the-same-event)
### Published event payload schemas
You can now look up what a built-in event payload actually contains instead of
guessing. Each documented event returns a real example payload per provider,
every property path with its type, and the merge tag that resolves it - plus
notes where the sample alone is ambiguous, such as which money fields are cents
and which are whole currency units. Available as `GET /api/v1/events/schemas`,
`sequenzy events schema`, and the `get_event_schema` MCP tool. Custom events
keep working exactly as before: they carry whatever properties you send and are
never validated against this list.
[View documentation](/concepts/events#looking-up-a-built-in-payload)
Popup and form block writes over the API, CLI, and MCP now reject a property
the block kind does not have, naming it (`blocks[3].content`), instead of
dropping it and saving an empty block. Every content kind's properties are
documented too - a `custom-html` block carries its markup in `html`, not
`content`.
[View documentation](/api-reference/widgets/update-popup#content-blocks)
A popup whose image panel has no usable image now falls back to the accent rail
and keeps the content full width, instead of showing a blank column beside it.
Setting the `image` visual style without an https `imageUrl` is rejected up
front, so the state cannot be saved by mistake.
[View documentation](/concepts/cli#popups)
Branch paths now reject an unrecognized step type by name - including a branch
nested inside a path - instead of treating it as an email and asking for a
subject. The error points at the supported step types, and nesting a branch on a
path is documented as a second `insert_sequence_step` call that reconnects to
the shared steps you already built.
[View documentation](/concepts/mcp-tools#branches-inside-a-branch-path)
Creating a company with a company-scoped API key (`seq_live_...` or a legacy
`ek_...` key) now fails with a clear error instead of quietly creating a
workspace that key can never open. Use a personal account key (`seq_user_...`)
from Account -> API Keys, or create the workspace in the dashboard.
[View documentation](/api-reference/companies/create)
### Random splits for sequence A/B tests
A Branch step can now route by percentage instead of by condition, so you can
run a true concurrent A/B test inside a sequence - for example sending half of
your abandoned-cart contacts a discount offer and half a free-shipping offer at
the same time, rather than running duplicate sequences one after another and
comparing across different weeks. Add a **Percentage Split** from the dashboard
sequence builder, or set `splitMode` to `random` with `randomPercentages` through
the API, CLI, or MCP. Variants that differ only in the email content are still
better served by an A/B test on that step.
[View documentation](/concepts/sequences)
### Ask AI steps can read a contact's recent activity
An Ask AI step can now include the contact's latest events, newest first, with
the count you pick (1-50), so a prompt can react to what someone actually did
rather than only to the event that started the sequence. The Contact data
section is now a short list of questions - trigger event data, recent activity,
custom attributes, tags - with each source's settings appearing only once you
turn it on, and new steps start with trigger event data included. Adding a step
now walks through Prompt, Contact data, and Output, with Cmd+Enter to move on
and arrow keys to jump between sections. Generated copy also stays a line or two
that your email drops in, without em dashes or marketing filler. Previews fall
back to example event data when a contact has no stored enrollment, so a step
can be checked before the sequence has ever run, and you can tune the prompt by
saying what should be different about a generated line. Ask AI generations are
free of charge for the rest of this week while the step is in beta - pricing
after that is still being decided, and you will be told before anything is
billed.
[View documentation](/concepts/sequences#call-ai-steps)
Brand logos in generated emails are now legible for dark-themed websites: logo files that only paint through the website's own styling are skipped in favor of a usable variant, and white dark-mode-only logos automatically get a dark backdrop behind them.
AI-generated emails now mirror your brand's own emoji usage - brands whose copy never uses emojis get emoji-free emails by default - and numbered section headings and icon lists render with tighter, better-proportioned alignment. Section label chips and pricing badges also follow your brand's corner style now: brands with a rectangular design language get square labels (or plain uppercase text labels across a sequence) instead of rounded pills. Every company also gets a consistent visual identity across all of its generated emails: section labels render in one brand-derived style everywhere (a rounded chip for rounded brands, a plain uppercase label for square ones), every email opens with the same small kicker treatment, quotes are never framed in stray dividers, and the logo size stays identical from email to email and sequence to sequence.
### Property filters for sequence stop conditions
Event stop conditions can now match on event properties: a `matchConfig` with
`propertyFilters` (for example `quota_used greater_than 1`) stops the sequence
only when a later event satisfies every filter, using the same operators as
event trigger filters. Event stops also now only count events received after
enrollment, so a sequence that triggers on an event can stop on the same event
name without cancelling itself at the trigger. Available in the dashboard's
stop-condition editor and via the API, CLI, and MCP, with validation errors
that spell out the expected `matchConfig` shape.
[View documentation](/concepts/sequences#stop-on-a-later-event-with-property-filters)
Searching contacts by a full email address is now an exact lookup rather than a
substring scan, so it returns instantly on large audiences instead of timing out
across the dashboard, the API, the CLI, and MCP. Partial terms such as `gmail.com`
or a first name still search across email, external ID, name, and tags as before.
[View documentation](/api-reference/subscribers/list)
Migrations that fail because your provider rejected the API key now say so
straight away, and tell you to reconnect with a key that has the required read
permissions, instead of stalling through several retries and then asking you to
try again.
The image on an image card can now be hyperlinked: set an Image link in the
card settings and clicking the photo opens that URL, independently of the
card's button. [View documentation](/concepts/visual-blocks)
A freshly inserted image card now shows an Add image prompt in its image slot,
and image card, hero, pricing, and product buttons no longer render as blank
pills or stay hidden while you are still filling in their link.
The Ask AI step editor is now organized into Prompt, Contact data, Output, and
Advanced sections, and opens pre-filled: a fast model drafts a starting prompt,
output fields, and context selection from what the sequence already knows, ready
to review, regenerate, or replace with a blank slate.
### See which link each recipient clicked
The Sent emails list now shows the destination under every recipient whose
latest status is "Clicked", and a send's detail page lists every link that
recipient opened, so a click no longer reads as "probably the main button" when
it was a footer or social icon. These views follow the recipient's own click:
if a security scanner sweeps the links afterwards, the row still describes the
human click that set the status. Open counts also now say plainly that bots are
already excluded while privacy features such as Apple Mail Privacy Protection
still prefetch tracking pixels, which is why a fresh send's opens arrive within
minutes and read high. [View documentation](/concepts/metrics)
### Retry transactional sends without duplicate email
Transactional sends now accept an `Idempotency-Key` across the REST API, CLI,
and MCP. Repeating the same logical request returns its original delivery ID
for 14 days, including after a temporary queue failure, while using the key for
different content is rejected. Authenticated `/api/v1` requests now also enforce
the documented shared limit of 100 requests per minute per API key with a
20-per-second burst limit and standard retry headers.
[View documentation](/api-reference/transactional/send)
### Emails that look like your website
Connecting a website now derives much more of your brand into the email theme:
the real call-to-action color and fill style of your site's buttons (solid or
outline), their corner radius, padding, and label weight, your heading scale,
weight, and letter spacing, heading/body font pairings mapped to email-safe
stacks, your site's spacing density, text and card colors, and your wordmark
logo sized correctly for email headers. Landing page previews and onboarding
both use the richer theme, and every value stays clamped to email-friendly
ranges with readable contrast. The new theme fields are also editable through
the API, CLI, and MCP.
[View documentation](/concepts/email-themes)
### Steer how AI designs your emails
Your workspace now has a **design prompt** alongside Tone & Voice: free-form art
direction that tells Sequenzy AI how your designed emails should be composed -
layout and density, which sections belong in an email, imagery, and how
prominent the call to action is. Tone & Voice shapes the writing; the design
prompt shapes the design, and it overrides Sequenzy's default composition rules
when the two disagree. Edit it in Company Settings under AI Context, or set it
through the API, CLI, and MCP.
[View documentation](/concepts/email-themes)
### Ask AI steps in sequences
Sequences can now generate personalized text per contact with a new **Ask AI**
step. Write a prompt with merge tags, choose exactly which contact data the
model may see (tags, trigger event data, specific attributes), and define the
named fields it must return - later emails insert them with
`{{ai.KEY.field}}` merge tags, with your fallback text used if generation ever
fails. Preview the output against real contacts before activating, and let
"Draft for me" write a starting prompt from what the sequence already knows.
Available in the builder and with full parity over the API, CLI, and MCP.
[View documentation](/concepts/sequences#call-ai-steps)
### Email editor fixes for links, formatting, and unsaved work
Links typed as a bare domain (`example.com`) are now saved with `https://`, so
they no longer arrive as dead links in the delivered email. Bold, italic, colour
and links applied to a testimonial quote or author line are kept instead of
being dropped when you click away, and fields that can only hold plain text no
longer offer the formatting toolbar. Emptied fields keep a visible placeholder
so they can still be clicked, size and spacing inputs can be cleared and retyped
without snapping to zero, and closing the tab with unsaved changes in a campaign
email now warns you first.
### Inline CID images survive an SMTP relay
Emails relayed through Sequenzy SMTP now keep their embedded images embedded. A
`multipart/related` message with `` used to have its images
flattened into `data:` URIs, which Gmail renders as a broken image; the parts are
now forwarded and re-sent as related attachments with the original Content-ID.
Ordinary file attachments are forwarded too (up to 10 files, 7MB total), and the
transactional API gained an attachment `contentId` field for the same effect.
[View documentation](/send-email/smtp)
### Simpler campaign audience targeting over the API, CLI, and MCP
Campaign create, update, and schedule now accept a plain `listIds` shorthand
(`--lists` in the CLI) to send straight to one or more lists without building
a `targetLists` object, and a malformed `targetLists` now returns an error
that names the offending field and the accepted shapes instead of a bare
"Invalid input". Campaigns created or updated through the API also clone your
default footer component now, matching emails built in the editor.
[View documentation](/api-reference/campaigns/schedule)
The Insert Image dialog is now fully keyboard driven: left and right switch
source tabs, down drops into the results, arrow keys move between thumbnails,
Enter selects the highlighted image, and Cmd+Enter (Ctrl+Enter on Windows)
runs the current tab's action.
Fixed nested repeat blocks placed inside a column or a conditional branch:
their item values (like `{{priceband.total}}`) rendered blank instead of the
row's data.
Fixed the Preview tab dropping nested repeat rows when the preview data also
held a leftover value named after the repeat's item alias - the editor showed
the rows, but the preview rendered none.
Long merge tags like `{{shows.performance_starts}}` now wrap inside
multi-column layouts in the editor instead of stretching their column and
pushing the neighboring columns out of view.
### Share any email with a public "view in browser" link
Campaigns, transactional emails, sequence emails, and standalone templates can
now be shared with anyone through a public link - like the classic "view in
browser" page, but anonymized instead of tied to one recipient. The hosted
copy renders with a sample contact, an inert unsubscribe link, and no open or
click tracking, so you can safely forward it to stakeholders, clients, or
people outside your audience. Click Share in the campaign, transactional, or
email editor to open the public page - the link is created automatically on
first use, and drafts work too, so you can share before anything is sent.
Links can also be managed from the API (`/api/v1/campaigns/{id}/share-link`
and `/api/v1/templates/{id}/share-link`), with `sequenzy campaigns
share/unshare` and `sequenzy templates share/unshare`, or through the
`share_campaign` and `share_template` MCP tools.
[View documentation](/api-reference/campaigns/share-link-create)
### Clearer automation flowcharts
The sequence flowchart now keeps chained conditionals readable: a nested
branch's paths merge back onto their own lane instead of jumping to the center
of the canvas, so steps no longer render on top of each other and connectors no
longer cut across unrelated branches. Branch paths also sit closer together -
each gap is now sized from the two paths beside it rather than the widest path
anywhere in the split - so wide flows need far less horizontal scrolling. You
can also zoom out much further or use fit view to see a large flow on one
screen.
### Rotate an API key without giving away key management
A key that lacks the `api_keys:manage` permission still cannot create or revoke
keys - that stays closed so a leaked key can never mint a full-access successor.
But it can now hand the work to you: `request_api_key_handoff` in MCP and
`sequenzy api-keys handoff` return a dashboard link that opens the create-key
form with the requested name and permissions already filled in. Add
`--replaces current` and the dashboard offers to revoke the predecessor once the
replacement exists, so a rotation finishes in one visit with you confirming it.
[View documentation](/api-reference/api-keys/handoff)
The sequence canvas also picked up a set of quality-of-life improvements: a
dot-grid background, duplicate and delete buttons right on each step card when
you hover it, and a keyboard shortcut overview - press `?` to see every
shortcut.
### Edit your email footer from the API, CLI, and MCP
The default email footer and the rest of your saved email components are no
longer dashboard-only. You can now read and replace them over the API
(`/api/v1/email-components`), with `sequenzy components footer get/set`, or
through MCP, so an agent or script can change the footer every new email is
built with instead of hand-writing a footer band into each email. A default
footer always keeps its unsubscribe link enabled, and existing emails keep the
footer they were built with.
[View documentation](/api-reference/email-components/get-default)
### Retry a PostHog history import from anywhere
A PostHog event-history import no longer fails over a handful of unusable
subscriber addresses: rows whose address is invalid, disposable, or already
bounced are skipped and counted, while real infrastructure failures still stop
the import loudly. If BullMQ exhausts retries or declares an import terminally
stalled, the integration is marked failed and can be retried from the dashboard,
the API, the CLI, or MCP through the same sync action. Retries restart from the
beginning and already imported events are deduplicated, so nothing is written
twice.
[View documentation](/integrations/posthog)
### Multiple SMS numbers with per-number sender identity
Workspaces can now hold up to 5 toll-free numbers: add one from Settings ->
SMS and pick which number each SMS campaign sends from, right in the
campaign's Message step. Every number can carry its own label (like
"Marketing" or "Support") and its own brand prefix override, so one workspace
can text under several sender identities. Labels and prefixes can also be
managed through the REST API, CLI, and MCP; sequences and one-off sends keep
using your default number.
[View documentation](/concepts/sms)
### Run the Supabase user backfill without the dashboard
The Supabase user sync is now available from the API, CLI, and MCP through the
existing sync action, so an agent preparing a campaign can import users who
existed before the database trigger was installed instead of asking you to open
Settings. It reads the project, schema, and table already saved on the
integration, so choosing the source stays a one-time dashboard step.
[View documentation](/integrations/supabase)
### Target every active Stripe price
Stripe product sync now imports every active price of a product as a variant
(the variant ID is the Stripe price ID) and no longer reports an archived
default price as the product's price, so purchase-triggered emails can target
the exact offer you currently sell.
[View documentation](/api-reference/products/list)
A sent campaign now shows the date and time it went out next to its Sent badge,
with its recorded pacing - no pacing recorded, spread over a set number of
hours, or at each recipient's optimal send time - available from the adjacent
info button. Imported campaigns may not carry pacing metadata from their source
provider. Hovering the Sent badge in the campaign list gives you the same record
without opening it, and the pacing fields come back from the campaigns API, CLI,
and MCP. [View documentation](/api-reference/campaigns/get)
Creating or updating a segment now returns a warning when an attribute filter references a custom attribute no subscriber has a synced value for - including a casing suggestion - instead of silently matching zero subscribers (or everyone, for empty checks).
The `html.` merge tag prefix also renders subscriber custom attributes as raw
HTML - `{{html.subscriber.body_html}}` in a campaign body sends each recipient
their own stored markup unescaped, now covered in the
[campaign personalization docs](/concepts/campaigns#raw-html-attribute-values).
Variable pickers now suggest every custom attribute in use across your account instead of only the ones carried by your most recent contacts, so typing an attribute set on older subscribers finds it in the email editor, SMS composer, and segment builder.
Change Plan now applies your new tier immediately from the upgrade dialog on every Stripe plan, including older grandfathered ones that previously redirected to the billing portal. Your locked-in rates carry over to the tier you move to.
### Release contacts past a sequence step in bounded waves
You can now move a capped batch of enrolled contacts from one sequence step
onto another over the API, CLI, and MCP, instead of cancelling and re-enrolling
them. The enrollment keeps its entry event properties, stop-condition
snapshots, and start date, and each release can carry a daily cap, a sort
order, tags, and a note that later shows up on the enrollment. It defaults to a
dry run and works even while new enrollment is paused.
[View documentation](/api-reference/sequences/move-enrollments)
### Realign waiting contacts after a sending window change
Changing a live sequence's sending window used to leave everyone already
waiting on an email-bound delay step on their old time, and narrowing it pushed
them to the next allowed day - a full extra week on a weekly sequence. You can now realign
them instead, from the API, `sequenzy sequences realign-enrollments`, or the
`realign_sequence_enrollments` MCP tool. Waits move to the window's opening on
the day they were already scheduled for, never onto a different day and never
earlier than now, and nobody is cancelled or re-enrolled.
Sequence windows never advance SMS, webhook, or other non-email actions.
Applied realignments run as background jobs with pollable status, so large live
sequences do not hold an API request open while contacts are re-queued.
[View documentation](/concepts/sequences)
### Opt-out dates and auditable unsubscribes
Subscribers now carry `unsubscribedAt`, the date the contact opted out, across
the API, CLI, and MCP tools, and you can narrow a list to a window of opt-outs
with `unsubscribedAfter` and `unsubscribedBefore`. Unsubscribes are also
recorded in the contact's activity timeline and unsubscribe stats even when
unsubscribe link tracking is turned off - previously that setting silently
suppressed the record as well as the link rewriting.
[View documentation](/api-reference/subscribers/list)
### Manage popups from the API, CLI, and MCP
On-site signup popups are no longer dashboard-only. You can now list, read,
create, update, duplicate, and delete them over the public API, with matching
`sequenzy popups` CLI commands and MCP tools, and get back the one-line script
tag that deploys them. Trigger, page targeting, schedule, and display frequency
are all editable, and setting a popup to `draft` stops it showing without
losing its view and conversion counts. Every popup now also reports its full
funnel - shown, started, submitted - with conversion rates. Existing
`widgets`-scoped API keys work unchanged.
[View documentation](/api-reference/widgets/create-popup)
SMS messages also got shorter: tracked links now use the dedicated `sqzy.io`
domain and the first-message compliance footer is 10 characters tighter, so
more of your copy fits in a single billed segment.
Feedback submissions from the API, CLI, and MCP server can now carry structured
reproduction detail - user intent, the ordered tool calls that led to the
problem, expected vs. actual outcome, and affected resource IDs - so agents can
report exactly what went wrong. [View documentation](/api-reference/feedback/submit)
The SMS add-on volume slider now runs all the way to 500,000 credits a month,
with new bundles at 100k, 150k, 200k, 250k, 300k, 400k, and 500k credits that
bring the rate down to \$0.0106/credit. [View documentation](/concepts/sms)
### Clearer sending-domain readiness in the API and CLI
Sending-domain responses now answer "are my DNS records right?" and "can this
domain send?" as two separate questions. When DNS is verified but the domain
is still activating, `verify` says so instead of claiming it is ready, and
`readiness.reason` reports a customer-actionable reason (`activation_pending`,
`activation_failed`, `sending_unavailable`, or a `dns_*` reason). The CLI
gained `sequenzy websites verify` to run a fresh DNS check from the terminal.
The internal fields `readiness.homeTransport`, `homeTransportReady`, and
`homeSesRegion` were removed from the public payload.
[View documentation](/api-reference/websites/verify)
### Wait until a weekday window in sequences
Sequences can now hold a contact until the next occurrence of a chosen weekday
inside a local-time window - for example, next Sunday between 09:00 and 12:00
in America/Los\_Angeles. Contacts that reach the step while the window is open
continue immediately; everyone else waits for the next occurrence, so a Friday
enrollment waits until Sunday morning rather than sending right away.
This reschedules the flow itself, which makes it different from a sequence
sending window: a sending window holds each email until the next allowed local
time, while a weekday wait moves where the contact sits in the graph, including
contacts handed off from another sequence mid-week. Weekly digests, Sunday-only
rituals, and day-specific programs can pin their send day by placing one delay
step immediately before the email.
Configure it on any Delay step in the dashboard by setting **Wait** to "Until a
weekday" and choosing the days, local-time window, and timezone. The same step
is available via the [API](/api-reference/sequences/create)
(`waitUntilWeekday` on a step, or a delay with `mode: "until_weekday"`), the
CLI (`sequenzy sequences create/update` step JSON), and the MCP sequence tools
(`create_sequence`, `insert_sequence_step`, `update_sequence_node`). If the
platform ever misses a wake-up past the window's end, the step rolls to the
next configured day instead of resuming late. [View documentation](/concepts/sequences)
### Poll responses per respondent
Poll and NPS answers can now be read one respondent at a time, not just as a
distribution. `GET /api/v1/metrics/campaigns/{id}/poll-responses`, the
`list_poll_responses` MCP tool, and `sequenzy poll-responses --campaign `
each return one row per respondent per poll block, newest answer first, with
their email, the answer and its stored value, the subscriber attribute it was
saved to, and the response time. Scope to one poll with `blockId`, or pass a
sequence email step's automation node ID to read that step's answers. Only each
subscriber's latest answer per block is returned, so the counts match the
campaign stats summary. This is the only place a response time is available -
the poll attribute holds the latest answer to any email and carries no
timestamp. [View documentation](/concepts/polls)
Subscriber search now filters by custom attribute. `search_subscribers` accepts
`attribute` (written as `"attributeName:value"`) with an optional
`attributeOperator` for `contains`, numeric comparisons, or `is_not_empty`, and
the CLI gained the matching `sequenzy subscribers list --attribute` /
`--attribute-operator` flags. Since poll answers are stored as attributes,
finding everyone who answered a given poll no longer means paging the whole
audience.
MCP tools now reject arguments they do not support instead of ignoring them. An
invented `filters` or `sortBy` used to come back as a full, unfiltered result
set that looked like a real answer; the call now fails with the list of
supported arguments and, where it applies, the supported way to express the
same request. `list_templates` also documents the `label` filter it already
accepted.
The `search_subscribers` MCP tool and `sequenzy subscribers list` can now filter
by a custom attribute (`attribute` with `attributeValue` and an optional
`attributeOperator`, including `is_not_empty` for "has this attribute set at
all"), so enumerating everyone with a given attribute no longer needs a
throwaway segment. Both surfaces also accept `cursor`, `offset`, and `page`
alongside `limit` and report `hasMore`, `nextCursor`, and `nextOffset`, so a
large audience can be read in chunks instead of only ever returning the first
page.
Listing templates is no longer capped at 50. [`GET /api/v1/templates`](/api-reference/templates/list),
`list_templates`, and `sequenzy templates list` now accept `limit` (up to 100)
and `offset` and return a `pagination` block with the full `total`. Every
campaign and transactional body counts toward that list, so accounts with a long
sending history could previously not reach anything older than their 50 most
recent emails. `list_templates` also gained the `label` filter its API has always
supported.
**Safer agent access** now includes `lists:write` and `tags:write`, so an agent
key can create the list and tags a new opt-in path needs instead of reusing an
unrelated existing list. Defining a list or tag is setup, not contact
mutation: changing who is in a list still requires `subscribers:write`, and
deleting one still requires `lists:delete` or `tags:delete`. Existing keys are
unchanged - [edit their permissions](/authentication) to pick this up.
Removing subscribers from a list now requires `subscribers:write` alongside
`lists:write`, matching what adding them has always required. Keys with
`lists:write` alone can still create, rename, and read lists.
### Multi-select polls
Poll blocks can now let recipients pick several answers instead of one. Turn on
**Multiple answers** in the poll settings (or set `"allowMultiple": true` on the
block via API, CLI, or MCP) and every answer link opens a hosted selection page:
the option the recipient clicked arrives pre-checked next to their previously
saved choices, they check as many options as they like, and a single save
records the whole set - no confirmation page per selection. Ideal for interest
surveys and "select all that apply" questions that previously needed an
external form. Multi-select is available for text answers; image-answer polls
remain single-choice so the hosted page never drops essential visual context.
The subscriber attribute stores the list of selected values, so segments filter
it with **contains** (for example `interests` contains `yoga`), and saving again
later replaces the list. The `poll.answered` event and outbound webhook carry
the selection as `values`/`answers` arrays for sync rules, sequences, and your
own systems. Poll Results, the campaign metrics API, `sequenzy stats --campaign`, and the `get_campaign_stats` MCP tool report multi-select blocks
per option with respondent counts as the denominator, and answer rows still
drill into the exact respondents. Because the selection page records nothing
until the save, link-scanning bots cannot fabricate or destroy selections.
[View documentation](/concepts/polls)
Campaigns, transactional templates, and saved email bodies now report
`emailPreset` - the per-email Style > Format - in their get and list responses,
the same way sequence steps already did. Comparing branded vs minimal chrome
across your emails no longer needs a render call per email. Available on the
API, in `sequenzy campaigns/transactional/templates` output, and through the
matching MCP tools. Campaign create and update now accept that field too, and
`sequenzy campaigns create|update` expose it as `--email-preset` for native
block emails.
### Diagnose and restore paused sending from the API, CLI, and MCP
When a workspace is paused for a high bounce or complaint rate, every send stops
* campaigns, sequence steps, transactional email, and test sends. Until now that
state lived only in the dashboard banner, so anyone working through the API, CLI,
or an agent saw sends fail with nothing to explain why and no way to recover.
`sequenzy sending status`,
[GET /api/v1/sending-status](/api-reference/sending-status/get), and the
`get_sending_status` MCP tool now return the whole picture: the pause reason, your
measured permanent-bounce, temporary-bounce, and complaint rates against the
thresholds that apply at your volume, the state of the automated sender-health
review, whether you can restore sending yourself, and ordered remediation steps.
Reading it needs only the `account:read` scope, so a read-only monitoring key can
diagnose a blocked account.
The response also states plainly what the rate is measured over. Sender-health
enforcement uses all-time totals counted from a reset watermark, not a rolling
window, so a paused rate never expires and waiting does not restore sending -
`metricsWindow.expiresAt` is always `null`.
Once the cause is fixed, `sequenzy sending resume --confirm-list-sanitized`,
[POST /api/v1/sending-status/resume](/api-reference/sending-status/resume), and
the `resume_sending` MCP tool restore sending. These are new clients of the same
control behind the dashboard's Resume sending button, with the same gates: only a
permanent-bounce pause qualifies, the automated review must have cleared it, and
suppressions are never removed. Resuming resets the bounce watermark so your rate
is recalculated from later sends, and requeues paused campaigns and due sequence
steps. When resume is not available, the response names the blocking gate instead
of failing opaquely.
Test sends now fail fast too. `send_test_email` and `send_sequence_test_email`
previously accepted the request and dropped the job later, reporting success for
an email that never sent; they now return the pause reason and the steps to clear
it. [View documentation](/guides/sender-health)
### Preview your email the way Gmail, Outlook, and Apple Mail will render it
The Preview tab can now simulate Gmail, Outlook (web and classic Windows), and
Apple Mail, including common font, CSS, and Gmail clipping differences. The
pre-flight checker also flags client-compatibility risks such as unsupported
backgrounds and layouts, animated GIFs, forms, and video. These previews are
estimates; classic Outlook's Word-based rendering remains approximate.
### Compare one sequence step across every sequence
"How many step-4 emails have we sent?" used to mean one stats call per sequence
and a spreadsheet, or paging the raw event stream and counting by hand. There is
now a single per-email view: one row for every campaign and every individual
sequence email step, each with its own delivery funnel, attributed conversions,
and revenue.
Sequence rows carry the sequence, the step number, and the step's node ID, so you
can total a step across sequences, rank every email in the account by open rate,
or find the steps driving revenue. Filter by email type, a list of sequences, or
a step position; sort and page as needed. Totals always cover every matching
email rather than the page you happen to be looking at, and counts read from
retained event storage - two years of events, not the 14-day delivery history -
so old sends stay answerable. Available as `sequenzy stats --emails`,
[GET /api/v1/metrics/emails](/api-reference/analytics/email-metrics), and the
`list_email_metrics` MCP tool. [View documentation](/concepts/metrics)
Getting from a step's numbers to its actual recipients is also direct now: the
sequence event stream and the sent-emails list both accept an `automationNodeId`,
so you can list who received one specific step instead of filtering a whole
sequence's history yourself. `sequenzy stats --sequence ` also prints each
step's sent, delivered, opened, and clicked counts, which the per-step data
already carried but the terminal output did not show.
Sending `fromEmail` with `fromName` over the API, CLI, or MCP now gives you that
display name even when the address already sends under another one: one mailbox
can carry several sender identities, so a single campaign or sequence can go out
as "Brennon at Example" while everything else keeps the shared name.
[View documentation](/concepts/sender-routing)
Numbered lists and bullets now keep their line breaks in the plain-text version of an email, instead of running together into a single line for the clients and filters that read that version.
The deliverability checker now points out a Reply-To on a free mailbox (Gmail, Outlook, iCloud and similar) that does not match your sending domain. Sequenzy still sends the message - the pattern is legal and common - but some filters score it as suspicious, so the checker flags it in case placement suffers.
A hidden form field left without a value now stores whatever your page submits for it, so machine-filled data such as UTM parameters lands on the contact; a hidden field with a value still pins that value and ignores the page. Form field blocks are also documented properly now - field types, `mapsTo` targets, choice options (only `value` is required), and hidden field behavior - and form validation errors name the property that failed, such as `blocks[3].options[0].value`. [View documentation](/api-reference/widgets/update-saved-form)
Poll blocks can now match a hand-designed brand email: set the answer buttons' corner radius (`0` for square corners on any style), pick the poll's font and the size, weight, tracking, and casing of the answer labels and question, recolor the question line on its own, and use your accent color with the Brutal style. Available in the editor's new Typography section and in block JSON via the API, CLI, and MCP. [View documentation](/concepts/polls)
### Fix an API key's permissions instead of replacing it
API key permissions can now be changed in place. Because a key's secret is only
shown once, picking a slightly-too-narrow permission set used to mean minting a
replacement key and re-wiring everything that held the old one. Now you edit the
key you already have: **Edit permissions** in Settings → API Keys, `sequenzy
api-keys update`, `PATCH /api/v1/api-keys/{apiKeyId}`, or MCP's
`update_api_key`. The key value does not change, so clients keep working, and
widened permissions apply to the very next request rather than after a cache
delay. Removed permissions may remain usable for up to five minutes while API
caches expire. Presets and scope lists replace the whole selection rather than
merging into it, so read the current scopes first.
Tagging existing contacts also got its own permission. **Safer agent access**
now includes `subscribers:tag`, which adds and removes tags on contacts that
already exist without granting the ability to create, update, or unsubscribe
them - so an agent key can reconcile a tag backfill without being widened to
full contact write access. Bulk tagging previously demanded `subscribers:write`
and `tags:write` even when every tag already existed; now `tags:write` is only
required when a request would introduce a tag name the workspace has never seen.
Existing keys that carry `subscribers:write` keep tagging with no change.
[View documentation](/authentication)
### Browse every SMS you have sent
A new **Sent SMS** page in the dashboard lists every text message your
workspace has sent, newest first, with the recipient, delivery status, message
preview, segment count, credits charged, and the campaign it came from. Search
by phone number or contact email, filter by status (delivered, failed,
skipped, suppressed, and the rest), and page through the full history. The
header totals show how many messages match your filters and how many credits
those messages charged, so you can answer "what did this send cost?" without
adding up rows.
Opening a message shows the full body with its line breaks, any attached MMS
media, a created/sent/delivered timeline, the failure or skip reason in plain
language, the credits charged versus quoted, segments, encoding, destination
country, sending number, and links to the campaign, sequence, and contact
behind it. The page appears in the sidebar under Activity once the SMS add-on
is enabled.
Subscriber timelines now show SMS activity too: sends, deliveries, failures
(with the error code), link clicks, and opt-outs appear alongside email
activity on each contact's page, with an SMS filter in the activity dropdown.
The same events are included in the subscriber activity returned by the API,
CLI, and MCP.
Two SMS billing changes also land today: included monthly credits now
**reset at the start of each billing period** instead of rolling over, and
**bundle upgrades are no longer prorated** - upgrading restarts your billing
cycle, charges the full new bundle price, and resets your balance to the new
bundle amount so every period starts clean. [View SMS
documentation](/concepts/sms)
### Turn on Shopify storefront tracking from the API, CLI, and MCP
The Shopify storefront pixel is what produces product views, collection views,
storefront search, cart activity, and browse abandonment. Until now it could
only be installed from the dashboard, which made it possible to build a
browse-abandonment or cart-recovery sequence that was correct in every respect
and could still never fire - the events it waited on were never being sent, and
nothing anywhere reported a problem.
Pixel state is now readable and fixable programmatically:
* [`GET /api/v1/integrations/{id}/pixel`](/api-reference/integrations/pixel-get)
reports whether the pixel is installed, which endpoint it posts to, whether
its endpoint and signed configuration are current, and the event names that
depend on it. A Shopify read error is reported as unknown rather than as a
confirmed tracking outage.
* [`POST /api/v1/integrations/{id}/pixel`](/api-reference/integrations/pixel-activate)
installs it, or repoints an existing pixel at your account. It is idempotent,
and returns `changed: false` when the pixel is already live.
* The CLI adds `sequenzy integrations pixel ` and
`sequenzy integrations enable-pixel `.
* MCP adds `get_integration_pixel` and `activate_integration_pixel`, and
`get_integration` now carries a `pixel` section for Shopify with a
prioritized recommendation that escalates to an error when a sequence is
already waiting on an event the pixel is not delivering.
Every read queries Shopify directly, because a merchant can remove the pixel
from the Shopify admin without Sequenzy hearing about it. Activation takes
effect on the next storefront visit and does not backfill the period the pixel
was off, so turn it on before building the sequence that depends on it. If the
store granted an older permission set, activation fails with a message naming
the reconnect step instead of quietly doing nothing.
[View documentation](/integrations/shopify)
[Double opt-in](/concepts/double-opt-in) can now be read and switched on
outside the dashboard: it is returned in the `consent` block of
[`GET /api/v1/tracking-settings`](/api-reference/tracking-settings/get), set
with `doubleOptInEnabled` on
[`PATCH /api/v1/tracking-settings`](/api-reference/tracking-settings/update),
and available as `sequenzy tracking update --double-opt-in on` and the
`update_tracking_settings` MCP tool.
### Filter analytics by mailbox provider
Metrics can now be segmented by recipient mailbox provider - Gmail, Microsoft
(Outlook/Hotmail), Yahoo, iCloud, Orange, and every other provider your
audience uses. A new provider dropdown on the Metrics page and on each sent
campaign's stats view scopes sends, deliveries, opens, clicks, bounces,
unsubscribes, top countries, best emails, and clicked links to that provider's
recipients, so you can see at a glance whether a deliverability or engagement
problem is provider-specific. The dropdown only lists providers you have
actually sent to, ordered by volume.
The same filter is available programmatically: pass `mailboxProvider` to
[`GET /api/v1/metrics`](/api-reference/analytics/metrics) or
[`GET /api/v1/metrics/campaigns/{campaignId}`](/api-reference/analytics/campaign-metrics),
or use `sequenzy stats --mailbox-provider gmail` in the CLI. Replies,
conversions, and attributed revenue cannot be segmented per provider and are
reported as 0 (or hidden in the dashboard) while a provider filter is active.
Repeating-content blocks now have a **Custom space between blocks** item style
setting, so you can tighten (down to 0px) or widen the vertical gap between
the image, title, and button inside each repeated item - in the editor,
in sent emails, and via `layout.itemStyles.contentSpacing` in the API.
Campaign responses across the API, CLI, and MCP now include `emailId`, the campaign's linked email body, so you can look up a dashboard-designed email in the [templates API](/api-reference/templates/list) and reuse it as `templateId` when creating campaigns programmatically.
You can now set a contact's native phone number when updating them - `phone` on the [update subscriber API](/api-reference/subscribers/update), `--phone` on `sequenzy subscribers update`, and `phone` on the `update_subscriber` MCP tool - instead of only at creation time. All three surfaces also take a new `phoneCountry` hint (for example `IT`) so national-format numbers from another CRM no longer have to be converted to E.164 first.
Pulling a large audience is dramatically faster and no longer times out. [`GET /api/v1/subscribers`](/api-reference/subscribers/list) now accepts `limit` up to 1000 and returns `pagination.nextCursor` on every response - follow it instead of incrementing `page` to walk a full list in a fraction of the requests, with results that stay stable while contacts are being added or removed. The CLI and the `search_subscribers` MCP tool use it automatically, so full-account backups of tens of thousands of contacts now complete reliably.
### Rename a sending identity without changing your defaults
Sender and reply-to profiles can now be renamed directly:
`PATCH /api/v1/sender-profiles/{id}`, `sequenzy sender-profiles update`, and the
`update_sender_profile` MCP tool all change a profile's display name in place.
Renaming was previously only possible through the company update, which renames
the profile it simultaneously promotes to your account default. That made it
awkward to standardize a display name across the several identities one mailbox
can carry - consolidating something like "Viraj from SnapCount" down to
"SnapCount" moved your default From selection around on every call. The new
endpoint touches the name and nothing else: the address, its sending domain, and
your default From/Reply-To selection stay exactly as they were, and campaigns
and sequences pinned to that profile pick up the new name on their next send.
Because a company cannot have two sender identities with the same name on one
address, a rename that would collide is rejected and tells you which profile
already holds the name. Renaming requires the `companies:manage` scope.
[View documentation](/api-reference/sender-profiles/update)
### Connect integrations from the API, CLI, and agents
Integrations that only need an API key or webhook secret - Polar, Paddle,
Dodo Payments, Whop, Creem, Chargebee, Clerk, PostHog, and Affonso - can now
be connected without opening the dashboard. `POST /api/v1/integrations/connect`
validates the credentials against the provider where possible, stores them
encrypted, queues the same initial backfills as the dashboard flow (revenue
sync for payment providers, affiliate backfill for Affonso, optional event
history import for PostHog), and returns the webhook URL to configure at the
provider. The CLI exposes it as `sequenzy integrations connect `,
with environment-variable secret input to keep credentials out of shell
history, and agents get the matching `connect_integration` MCP tool. The
integration catalog now lists `connectFields` per provider, so you can
discover exactly what each one needs before connecting. OAuth and app-install
providers (Stripe, Shopify, Supabase, GitHub, WooCommerce) still connect in
the dashboard, since those flows need a browser.
[View documentation](/api-reference/integrations/connect)
Historical event imports (PostHog history and backdated API events) now also
move each contact's signup date back to their earliest imported event - never
later - so migrated contacts stop showing as "added today" in date-added
segments and growth reports.
### Update saved forms from the API and agents
Saved signup forms can now be edited programmatically: `PATCH /api/v1/forms/{formId}`
and the matching `update_form` MCP tool rename a form, retarget its lists and
tags, edit its headline, description, button, and success copy, restyle its
theme to your brand colors, or replace its content blocks entirely - so an
agent can repair or restyle a published form without recreating it.
The Sequenzy CLI exposes the same update workflow through `forms list` and
`forms update`, including JSON/file inputs for theme and block changes.
`create_form` also accepts the same `theme` overrides, so new forms come out
in your brand instead of the default accent. In the builder, blocks added from
the Add panel now land above the submit button instead of at the bottom of the
form. [View documentation](/api-reference/widgets/update-saved-form)
### Deliver to Apple Private Relay subscribers
If your app uses Sign in with Apple, some of your contacts have an
`@privaterelay.appleid.com`, `@private.icloud.com`, or legacy relay `@icloud.com` address. Apple only forwards mail from domains you
have registered in your Apple Developer account, and it checks the Return-Path
domain rather than only the From address your recipients see - so mail to these
subscribers bounces until every source is registered, and the Return-Path domain
was not visible anywhere in Sequenzy.
When your audience includes relay subscribers, your sending domain settings
show an **Apple Private Relay** section
with the exact domains to copy into Apple's Email Sources, and these rejections
are recorded as temporary bounces labelled clearly, so the subscribers stay on
your list instead of being suppressed for what is a one-time setup step. Once
you register, the same addresses start delivering.
[View documentation](/concepts/apple-private-relay)
### See what every integration actually does
Integrations are no longer a list of connection statuses. The API, CLI, and MCP
can now explain what each provider does and what it is doing for your account
right now.
`integrations catalog` describes every provider whether or not you have
connected it: what it syncs, every event it can emit and the real-world moment
that produces it, the subscriber attributes it writes, and its caveats. Coverage
genuinely differs between providers, and the catalog says so plainly - only
Shopify has storefront browse tracking, only Stripe and Chargebee cover the full
trial lifecycle, and Polar emits no payment-failure event, so a dunning sequence
built on Polar would never have fired. Use it to compare providers before
picking one, or to check that an event your sequence needs actually exists.
`integrations get` takes that and crosses it with your own configuration. For
each event it shows the tags your sync rules apply, the conditions gating that
rule, and which of your sequences trigger on it - plus the events nothing is
listening to at all. That last list is usually the fastest way to find
automation you have not built yet: a store quietly emitting `ecommerce.price_drop`
with no sequence attached, or a payment-failure event with a dunning sequence
that exists but stopped accepting enrollments. Recent webhook activity and
prioritized recommendations sit alongside it, so "connected but nothing is
happening" has an answer instead of a shrug.
It also reports which events have actually reached you, not just which ones the
provider could send. That difference catches a failure nothing else does: an
integration that takes payments normally but has never once delivered
`saas.trial_started` is almost always a webhook missing a topic, and until now
it looked completely healthy because everything it does send arrives fine.
Two safe actions come with it. For payment providers that advertise sync
controls, `disable-sync` pauses future imports while keeping the connection and
its credentials, so turning it back on does not need another OAuth round trip.
An import already in progress must finish first. `sync` re-runs a payment
provider's customer and revenue import. Connecting and disconnecting stay in
the dashboard, because both handle stored credentials and provider-specific
cleanup.
Everything is available on all three surfaces: `GET /v1/integrations/{id}`,
`sequenzy integrations get`, and the `get_integration` MCP tool.
`sequenzy integrations guide` also brings the integration code examples that
were previously MCP-only to the terminal.
[View documentation](/api-reference/integrations/get)
Workspaces that use SMS now get a dedicated **SMS** page in the sidebar, with
their credit balance alongside it, and you can upgrade your monthly SMS
bundle any time from that page - the new credits are added to your balance
immediately.
### Get notified when someone subscribes
Sequenzy can now email you when a contact joins your workspace, and when a
campaign finishes sending. Both are off until you opt in, so turn on the ones
you want in Settings -> Notifications.
New-subscriber notifications arrive either per signup or as one daily summary
listing who joined. Campaign notifications arrive when a send completes, with
the final sent and failed counts. Each teammate chooses their own settings, in
Settings -> Notifications, and changing yours never changes anyone else's.
Two things keep the volume sane. Imports never trigger notifications, so
migrating 40,000 contacts produces no email at all. And if a workspace passes 50
real signups in a day while set to per-signup email, the rest of that day is
rolled into a single summary rather than filling your inbox.
The settings are available through the API, the CLI (`sequenzy notifications
get` / `set`), and MCP. For Slack, Zapier, or your own service, there is also a
new `subscriber.created` outbound webhook event carrying the contact's origin
`source` and the lists they joined.
[View documentation](/concepts/account-notifications)
### Agents can finish workspace setup on a safe key
The **Safer agent access** preset can now edit your workspace profile: the
company name, branding, the physical mailing address your email footers need,
and the product context and tone of voice that AI writing uses. Until now those
lived behind the same permission as creating a workspace, so an agent asked to
"set up my account and build my emails" would fail on the first step and need a
full-access key for the rest of the job.
Creating workspaces and changing your account-wide From and Reply-To defaults
still require the broader **Manage workspaces** permission, so an agent with a
safe key cannot silently repoint where your mail comes from. A request that
mixes the two is rejected as a whole rather than half-applied.
Keys keep the exact permissions they were issued with, so nothing you already
use changes behaviour: create a new key (or reauthorize your MCP connection)
with **Safer agent access** to pick up the new profile permission.
[View documentation](/authentication)
### PostHog event history import
Connecting PostHog now imports your existing event history, not just events
from the moment you connect. Enable **Import event history** in the connect
modal with a personal API key (Query Read + Project Read scopes) - Sequenzy
detects your region and project automatically and backfills up to 5 years of
identified events with their original timestamps. An event-based segment like "performed
`product.signed_up` in the last 30 days" is populated immediately instead of
starting from zero.
Historical events count for segments, campaign audiences, and the contact
timeline, but they never start automations - no sequences, waiting steps, sync
rules, or goal conversions. Events without an identified email are skipped, the
import is safe to re-run, and events that already arrived through the webhook
are never duplicated. A failed import shows a Retry button on the PostHog card
without ever affecting the real-time webhook connection.
The integration also gained real event settings: a picker that lists the event
names your project actually emits (loaded through the same API key) so you
choose your allowlist instead of typing names from memory, and new connections
now skip noisy PostHog-internal events like `$pageview` unless you opt in.
[View documentation](/integrations/posthog)
### Import history with its real dates
When you move to Sequenzy from another platform, your contacts and their events
no longer all arrive stamped with today's date. Subscribers can be created with
the date they originally signed up, and events can be recorded with the date
they actually happened, so audiences like "signed up in the last 30 days" or
"purchased in the last 90 days" are correct the moment your migration finishes
instead of months later.
Pass `createdAt` when creating a subscriber (single, bulk import, CLI
`--created-at`, or the `add_subscriber` and `create_subscriber_import` MCP
tools). An existing contact's signup date only ever moves earlier, never later,
so re-importing can add history but never erase it. Any list they join is dated
the same way.
Pass `occurredAt` on an event (single, bulk, CLI `--occurred-at`, or the
`trigger_subscriber_event` MCP tools) to record when it happened. An event more
than an hour old is treated as history: it is stored with its real timestamp and
counts for segments and the subscriber timeline, but it deliberately runs no
sequences, sync rules, waiting steps, goal conversions or webhooks. That is what
makes it safe to import years of history in one afternoon - otherwise every
imported purchase would enroll the contact and send a real email. Events within
the last hour behave exactly as before, so ordinary queue lag is unaffected.
**Set your flows up first.** You do not have to import before building your
sequences. Switch on everything you want, then bring your data over. Imported
contacts and events are kept out of the calculations flows use to decide who to
enroll, so a year of imported purchases will not satisfy a "bought 5 times in
the last 30 days" flow, and imported contacts will not look like new arrivals
to a "joined in the last 30 days" flow. They still count for the segments,
filters and reports themselves - only the enrollment decision ignores them, and
only for the imported records. Real activity after the import triggers your
flows exactly as normal.
Imports are safe to re-run. Give an event your own `eventId` and re-sending it
writes nothing new; without one, identical events are deduplicated by content.
Custom event history is now kept for five years, and a timestamp older than that
is rejected with a clear error rather than accepted and silently discarded.
[View documentation](/api-reference/subscribers/events/trigger)
### Segment suggestions
The Segments page now has a Suggested segments section that sticks around
after you create your first segment, instead of only appearing on the empty
page. It shows ready-made segments you haven't created yet, matched to your
business type, and each one opens prefilled in the segment editor.
Click **Suggest with AI** and Sequenzy goes further: it looks at what is
actually in your workspace - your tags, custom attributes, which events have
been firing recently, your lists, and the segments you already have - and
proposes segments built from that data. Every AI suggestion is checked against
your real subscribers before it is shown, so each card comes with a live
subscriber count and empty suggestions are filtered out. Nothing is created
until you click a suggestion, review the filters, and save it. Suggestions are
kept on the page across visits until you use them or generate new ones.
A segment on its own doesn't send anything, so every AI suggestion also comes
with a recommended next step, like "Send a VIP-only discount" or "Start a
win-back flow". Save the segment and one click takes you there: a new campaign
opens with its audience already set to the segment, or the sequence template
picker opens with the new sequence set to trigger when contacts enter the
segment - where the existing "enroll subscribers who already match" flow picks
up the people who are in it today.
### Phone-Only Contacts
You can now add contacts that have a phone number and no email address - for
SMS-first audiences moving over from another platform. Map a phone column in
the CSV importer and leave email unmapped, or create them through the API
(`POST /api/v1/subscribers` with only `phone`), the CLI
(`sequenzy subscribers add --phone '+14155550123'`), or the `add_subscriber`
and `create_subscriber_import` MCP tools. Phone-only contacts are
automatically excluded from email campaigns and sequence email steps, receive
SMS normally once they have consent, and deduplicate by phone number on
re-import. [View documentation](/concepts/subscribers#phone-only-contacts)
### Earlier SMS Sending With Recipient-Local Quiet Hours
SMS quiet hours are now evaluated in each US and Canadian recipient's own
timezone, inferred from their phone number's area code, instead of a single
nationwide window that opened at 11:00am Eastern. US numbers open at **8:00am
local time** - the TCPA floor - and close by 8:00pm local, with Sunday sends
held until noon for Texas numbers. Canadian numbers follow the CRTC's rules
instead: **9:00am to 8:00pm local on weekdays** and **10:00am to 6:00pm local
on weekends**. Numbers whose timezone can't be inferred keep the previous
conservative window, and out-of-window messages are still deferred, never
dropped. [View documentation](/concepts/sms#quiet-hours)
The sequence Subscribers dialog's Enroll tab now has a **Search** flow next to
the trigger-criteria one: look up any active subscriber by email and enroll
them from the first step, even when they don't match the sequence trigger. The
dashboard now matches what the enroll API endpoint and the
`enroll_subscribers_in_sequence` MCP tool could already do.
Sequence email steps created or updated through the API, CLI, or MCP now
validate their blocks against the block schema, the same way campaigns and
transactional emails already did. A block that is not a valid block is rejected
with the step, block index, and field named, instead of being stored and then
failing every enrollment at that step; blocks written with `buttonText` and
`buttonUrl` are accepted as aliases for a button's `text` and `url`. Advisory
`warnings` come back on success for fields that will not render as their name
suggests. [View documentation](/api-reference/sequences/create)
Transactional email templates can now be deleted outside the dashboard, with
`sequenzy transactional delete `, `DELETE /api/v1/transactional/{idOrSlug}`,
or the `delete_transactional_email` MCP tool. Deleting frees the slug for reuse,
keeps past deliveries and their stats, and leaves the email content behind as a
reusable template. The CLI also gains `sequenzy transactional update ` for
editing a template's name, subject, preview text, enabled state, or body, which
the API and MCP could already do.
[View documentation](/api-reference/transactional/delete)
Failed sequence enrollments now report `failedReason`, so you can see why an
enrollment stopped rather than only that it did. Filter with
`sequenzy sequences enrollments --status failed`, or the `status` argument
on the API and `list_sequence_enrollments`. The same reason repeated on one step
points at that step's content or configuration rather than at the contacts.
[View documentation](/api-reference/sequences/list-enrollments)
Sending an outbound webhook test through the API, CLI, or MCP now behaves like
the dashboard test button: the test always reaches the endpoint you named, even
when that endpoint subscribes to no event types, and the response returns the
queued delivery so you can check the result right away instead of waiting for it
to appear in the delivery list.
[View documentation](/api-reference/webhooks/outbound)
### SMS Campaigns
Workspaces with the SMS add-on can now send one-off text blasts, not just
automation texts. The Campaigns screen gains an Email / SMS switch at the top;
an SMS campaign walks through the same steps as an email campaign - message,
recipients, schedule, review - with a phone-style preview throughout. You pick
recipients with the same lists, segments, and filters as email campaigns, and
Sequenzy narrows the audience to contacts who can actually receive SMS
(consent, phone number, no suppression) and shows the estimated credit cost
before you confirm. Delivery respects per-recipient quiet hours, can be
scheduled or stopped mid-send, and campaign stats cover sent, delivered,
clicked, failed, skipped, and credits used.
[View documentation](/concepts/sms)
The Stripe integration can now import customers that have no subscription or
payment at all - useful when your app creates a Stripe customer for every
signup, including free-tier users. Enable **Sync customers with no subscription
or payment** in the Stripe setup screen; it stays off by default so
abandoned-checkout emails are never imported unless you opt in. These customers
arrive through the Stripe sync rather than webhooks, so ones created later show
up the next time you run **Sync Revenue**.
[View documentation](/integrations/stripe)
A WooCommerce store sync no longer stops at the first customer whose email
address cannot receive mail. Stores with real order history usually carry a few
addresses on domains that have since been shut down, and a single one of those
previously failed the whole import - leaving the integration in `failed` with
nothing brought in. Those buyers are now imported as suppressed profiles
instead, so their orders still count toward revenue history and segments while
nothing is ever sent to them. The affected addresses are listed under
`lastSyncSkipped` on the integration, visible in `Settings -> Integrations`, in
`sequenzy integrations`, and through the `list_integrations` API and MCP tool.
[View documentation](/concepts/woocommerce-plugin)
Writing email blocks for campaigns, templates, transactional emails, and A/B test
variants through the API, CLI, or MCP now returns a `warnings` array when a field
you sent is not part of that block's schema, or does not control
what its name suggests - for example `styles.backgroundColor` on a button, which
colors the band behind the button while the fill comes from `buttonColor`. Each
warning names the field and lists what that block does accept, so a successful
write that quietly ignored a field no longer looks like it took effect.
[View documentation](/concepts/visual-blocks)
Email renders across the API, CLI, and MCP now return `unresolvedMergeTags`, so
a misspelled merge tag is no longer indistinguishable from one that is simply
blank for the previewed contact - both render as an empty string. Rendering a
sequence step that follows a create-discount step also fills `{{discount.code}}`
and the other discount tags from that step's real configuration, using a
placeholder code, so discount emails can be checked before anyone is enrolled.
[View documentation](/api-reference/sequences/render)
Transactional templates can now insert a variable's HTML unescaped by
prefixing a merge tag with `html.`, as in `{{html.prerenderedHtml}}` - useful
for passing a prerendered order summary through the API while the template
keeps your branded header and footer. Regular `{{variables}}` stay
HTML-escaped, and injected HTML is sanitized.
[View documentation](/concepts/transactional-emails#raw-html-variables).
### Call Webhook Steps In Sequences
Sequences can now call your own API mid-flow and use the response in later
emails. Add a **Call Webhook** step from the builder, point it at any HTTPS
endpoint (`GET`, `POST`, `PUT`, `PATCH`, or `DELETE`), and give it a result key
like `lookup`. When a subscriber reaches the step, Sequenzy makes the request
with merge tags like `{{email}}` and `{{event.order_id}}` resolved in the URL,
headers, and JSON body, and saves the response for that enrollment.
Later emails can then insert fresh values with merge tags such as
`{{webhooks.lookup.data.status}}`, so details are looked up at send time
instead of being frozen when the sequence started. Useful whenever the source
data keeps changing between enrollment and send: wait for the right moment,
fetch the current values, then send the email that references them. The email
editor's variable picker offers these tags for every webhook step that runs
before the email, suggests the actual field names seen in recent responses
(with sample values) once the step has run, and prefills the preview with the
most recent response so you can see real data while writing.
You control what happens when the endpoint fails or times out: continue,
exit the sequence, or stop the enrollment with an error. Either way you can see
what came back: the step's completed list shows the HTTP status and the endpoint
called for each subscriber, flagging failures the sequence walked past, and
clicking a subscriber opens the full response including the saved JSON body.
Test runs show the same status line for the request they really sent.
Webhook steps can
also be created and edited via the API, CLI, and MCP (`insert_sequence_step`
with `stepType: "webhook"` now supports `body`, `resultKey`, and `onError`).
[View documentation](/concepts/sequences#call-webhook-steps)
Sequence branches can now stay empty: saving and activating no longer require
every branch path to have a step. Empty paths pass subscribers straight to the
next step, and the editor now shows where each empty path leads, highlights
empty paths on live sequences, and lets you remove an empty path in one click
when it leads to a step of its own.
[View documentation](/concepts/sequences)
Account-wide tracking settings - open, click, and unsubscribe tracking, the
default attribution window, and automatic UTM tagging - can now be changed
outside the dashboard through the API (`PATCH /api/v1/tracking-settings`), the
CLI (`sequenzy tracking update`), and MCP (`update_tracking_settings`).
[View documentation](/concepts/tracking)
Customers imported from Shopify and the WooCommerce plugin who already placed
orders are now tagged as e-commerce customers, so they appear in the
"E-commerce customers", "Repeat buyers", and other purchase-based segments
right after the initial sync - re-run the sync from the plugin to backfill
existing imports. WooCommerce initial syncs are also processed in the
background now, making them reliable on large stores.
TikTok and Pinterest can now be saved under Product Info and added to the social
block, so their icons render in your emails alongside the platforms already
supported.
If you publish a DMARC policy on your sending subdomain, domain verification now
reports its status and value against that exact record rather than the one at
your root domain, and automatic Cloudflare setup no longer reports success while
a problem there is still blocking verification.
### Landing Page Favicons, Search Visibility, And Duplication
Three additions to **Page settings** and the landing-page list.
**Your favicon, not ours.** Published pages showed the Sequenzy icon in the
browser tab. Now you can upload a favicon per page, and any page without one
falls back to your workspace logo automatically, so a customer never sees
Sequenzy branding in the tab.
**Hide from search engines.** Pages on a verified custom domain are indexable by
default, which is wrong for private client previews and internal launches. Turn
on **Hide from search engines** and the page serves a `noindex, nofollow` tag so
Google and other crawlers keep it out of search results. The link keeps working
for anyone you share it with. Landing-page domains also no longer serve
Sequenzy's own robots.txt sitemap.
**Duplicate a page.** Copy a page you already like instead of rebuilding it. The
copy is created as a draft with its own slug and stats, and the original keeps
its published URL and metrics.
All three are available through the API, CLI (`sequenzy landing-pages
duplicate`), and MCP (`duplicate_landing_page`, plus `seo.faviconUrl` and
`seo.hideFromSearchEngines` on page content).
[View documentation](/concepts/landing-pages)
Transactional API sends can now disable click or open tracking per send with
the new `trackingSettings` field, useful when tracking redirects break universal
links. [View documentation](/concepts/tracking)
### See who is inside a sequence right now
Sequence stats have always told you how many contacts sit at each step. You can
now pull the contacts themselves. A new enrollments listing returns one row per
enrolled contact with their email, name, the node they are currently on, when
they entered the sequence, and when a waiting contact resumes.
Filter by step, status, subscriber, or email address, sort by soonest resume to
see who moves next, and page through the full list to export it. This answers
the question counts cannot: not "how many are waiting at Wave 1", but "who".
It is available on all three surfaces:
* API: `GET /api/v1/sequences/{sequenceId}/enrollments`
* CLI: `sequenzy sequences enrollments `, including `--all --csv` for a
one-command export
* MCP: `list_sequence_enrollments`
[View documentation](/api-reference/sequences/list-enrollments)
Email segment filters now accept `is` and `is_not` for exact, case-insensitive
address matching in the dashboard, API, CLI, and MCP, so allowlists and
exclusions of specific contacts no longer have to be built out of `contains`
filters. [View documentation](/concepts/subscribers)
`fromName` and `replyToName` can now be sent on their own through the API, CLI,
and MCP to rename the display name of your default From and Reply-To profiles
without changing the address, and the new `senderProfileId` / `replyProfileId`
fields pick exactly which profile becomes the default when one address carries
several display names. [View documentation](/api-reference/companies/update)
Sequence reads across the API, CLI, and MCP now return an unambiguous
`effectiveStatus` (`draft`, `live`, `enrollment_paused`, `paused`, `archived`)
so a sequence that is `active` with paused enrollment can no longer be mistaken
for one that takes new subscribers. [View documentation](/api-reference/sequences/list)
Windowed delivery stats are now a funnel over the sends made inside the period,
so open, click, and reply rates can no longer exceed 100% when older emails are
opened or replied to during the window.
[View documentation](/api-reference/analytics/metrics)
AI sequence generation now writes up to 10 emails at once instead of 3, and the
progress dialog lists every step in sequence order with its live status.
### Segment by Purchased Collection
Segment on a whole collection instead of one product at a time. Pick
"Purchased Collection" in the segment builder and the segment matches everyone
whose orders contain any product in it, with the same bought / didn't buy /
order count options as the product filter. Works retroactively against your
existing orders, and is available through the API, CLI
(`--commerce-collection`), and MCP.
Membership is evaluated against your catalog as it stands now, so moving a
product out of a collection also removes its past buyers from the segment.
[View documentation](/concepts/cli#segments)
### Bulk Sequence Cancellation, Bulk Tagging, and Test Events
Three lifecycle-migration gaps are now covered across the API, CLI, and MCP.
Sequence enrollments can be cancelled with `cancelAll: true` or a batch of
`subscriberIds`, which is the only way to stop contacts already mid-flight -
pausing a sequence just blocks new entrants. Bulk targets default to a dry run,
and `sequenzy sequences cancel-enrollments --all --apply` pages through
large cancellations for you. Tags can be added or removed across up to 500
existing subscribers per request; unknown identifiers come back in `notFound`
instead of creating contacts, and tag automations stay off unless you ask for
them. And the new `trigger_subscriber_event` MCP tools emit the same events an
integration would, so triggers, branch conditions, and stop conditions can be
tested without waiting for real traffic.
[View API documentation](/api-reference/subscribers/tags/bulk-add) ·
[View CLI documentation](/concepts/cli) ·
[View MCP documentation](/concepts/mcp)
### Read-Only Account Audit Tools
You can now check how an account is wired up without holding any write access.
Four read-only surfaces across the API, CLI, and MCP cover connected
integrations and their sync health, sender and reply-to profiles with the
sending domain behind each address, tracking and attribution settings, and a
campaign's resolved audience. The audience check is the one to reach for before
a send: it turns stored list and segment IDs into names, flags references to
deleted records, recomputes the recipient count at read time, and warns when
targeting is unset, because scheduling such a campaign sends to every active
subscriber. Integration credentials, access tokens, and webhook secrets are
never returned.
[View CLI documentation](/concepts/cli) - [View MCP documentation](/concepts/mcp)
### Render Emails to HTML for Your Own Preview
If you build your own dashboard or email builder on top of Sequenzy, you can now
fetch the rendered email instead of only its block JSON.
`POST /api/v1/campaigns/{campaignId}/render`,
`POST /api/v1/sequences/{sequenceId}/nodes/{nodeId}/render`, and
`POST /api/v1/templates/{templateId}/render` return the exact email-safe HTML
that would be sent, plus the resolved subject, preview text, and locale. The same
renderer powers real sends, so an embedded preview cannot drift from what
recipients receive.
Pass `subscriberId`, an inline `subscriber`, or nothing at all to render for a
sample contact, and `variantId` to preview an A/B test variant. Links stay clean
unless you pass `tracking: true`. All three endpoints are read-only. The same
capability is in the CLI as `sequenzy campaigns render` (plus `sequences` and
`templates`), and as the `render_email` MCP tool.
[View documentation](/api-reference/campaigns/render)
### Campaign Audience Before Scheduling
Campaigns managed through the API, CLI, or MCP can now save and change their
audience without scheduling a send. `POST /api/v1/campaigns`,
`PUT /api/v1/campaigns/{campaignId}`, `create_campaign`, and `update_campaign`
all accept `targetLists` (or the `segmentId` shorthand), and `update_campaign`
accepts `targetLists: null` to clear saved targeting again. In the CLI,
`sequenzy campaigns create` and `sequenzy campaigns update` accept `--segment`,
`--target-lists-json`, and `--target-lists-file`, `campaigns update` adds
`--clear-target-lists`, and `sequenzy campaigns schedule` now accepts
`--segment` alongside its existing target list flags. Campaign reads return the
saved audience as `targetLists`, and `campaigns create`, `campaigns update`, and
`campaigns get` print it on the `Audience` line.
[View CLI documentation](/concepts/cli)
### Undeliverable Mail No Longer Counts as a Bounce
A bounce means the receiving provider told us the mailbox is bad. Until now, a
message that simply ran out of delivery routes was recorded the same way, so a
temporary problem on our sending infrastructure could mark a perfectly valid
address as bounced, stop future sends to it, and count against your bounce
rate.
These are now separated. When every delivery route for a message is exhausted,
the message is marked **Delivery failed**, with the explanation "The recipient
provider rejected all currently available delivery routes." A failed send:
* does not suppress the address, so the contact stays subscribed and receives
your next campaign;
* does not count toward your bounce rate, or toward the automatic sending pause
that protects your sender reputation;
* reports as a new `email.failed` webhook event instead of `email.bounced`, so
your own systems can tell "we could not get through" apart from "this address
is invalid".
Genuine bounces are unchanged. An unknown mailbox still bounces, still
suppresses the address, and still counts.
**If you use outbound webhooks, check your subscription.** Sends that would
previously have reached you as `email.bounced` now arrive as `email.failed`.
Endpoints created from now on include `email.failed` in the default event set,
but existing endpoints keep the exact event list they were created with, so add
`email.failed` to yours if you want to keep hearing about undelivered mail.
Deliverability dashboards now also break out Comcast, GMX/WEB.DE, Proton and
many more Microsoft, Yahoo, Orange, Free and SFR domains by name instead of
lumping them into "Other". Existing history keeps whatever provider it was
recorded under, so charts step-change on the day this ships rather than being
rewritten.
### Anonymous Shopify Browsing Is Backfilled After Identification
Most shoppers browse your store and add items to their cart before ever
typing their email. Sequenzy's Shopify browse pixel now remembers that
anonymous activity in the shopper's browser (for up to 14 days) and
backfills it onto the contact the moment they identify - by entering their
email at checkout or signing in. Product views, add-to-carts, cart views,
collection views, and searches all land on the contact's activity timeline
with their original timestamps, so you see the full journey that led to a
checkout instead of a contact who seems to appear out of nowhere. Once
identified, later visits from the same browser are tracked in real time,
which also makes browse- and cart-abandonment automations work for
customers who first showed up anonymously. Backfilled historical events
never trigger event-received sequences, never advance a sequence a contact
is already in, and never send abandonment emails retroactively. They do
update segment membership, so a contact can still enter a segment-triggered
sequence if your segment matches the backfilled activity.
[View documentation](/integrations/shopify#anonymous-visitor-backfill)
### Sequences Now Run for Unsubscribed and Bounced Contacts
Unsubscribed and bounced contacts are no longer blocked from entering
sequences. When a trigger matches, they enroll like anyone else and move
through every step: tag changes, attribute updates, webhooks, and emails
marked as transactional (an order-shipped notification, for example) all
still run. Marketing email steps are skipped automatically for them, so
consent is never violated. Contacts who unsubscribe while already inside a
sequence keep the previous behavior - the remaining enrollment is cancelled.
This makes event-driven flows like order updates work for every customer,
whether or not they are subscribed to marketing email.
[View documentation](/concepts/sequences#unsubscribed-and-bounced-contacts)
### Whop Integration
You can now connect your Whop account to sync payments and memberships
straight into Sequenzy. Once connected, purchases, failed payments,
cancellations, and refunds flow in as subscriber events, and each matching
subscriber gets revenue attributes (`mrr`, `ltv`, `whopCustomerId`) you can use
to build segments and trigger automations - for example targeting high-MRR
customers or launching a win-back sequence when a membership churns.
Connecting takes a Whop API key with the member, email, payment, plan,
access-pass, phone, and promo-code read permissions listed in the setup guide,
plus a webhook pointed at your Sequenzy company. Existing customers and
memberships are backfilled automatically on connect, and you can trigger a
manual re-sync from the integration card at any time.
[View documentation](/integrations/whop)
### Immediate Campaign Cancellation
Cancelling a sending campaign now stops outgoing emails within seconds:
batches already being processed check for the cancellation before every email
instead of finishing their full batch first.
### Live Sequence Enrollment Stage Counts
Sequence metrics now include `enrollmentCounts`, a live snapshot of active and
waiting enrollment runs grouped by current node. The same breakdown is
available from the REST sequence metrics endpoints, `sequenzy stats --sequence`, and the MCP `get_sequence_stats` tool, so agents can answer “how
many are waiting at each stage?” without scanning subscribers individually.
Historical date filters still scope engagement metrics while enrollment counts
reflect current state.
[View sequence metrics documentation](/api-reference/analytics/sequence-metrics)
### Stable DNS for Unified Sending Domains
Domains created in the new unified cohort receive one Sequenzy-owned DKIM,
SPF, and bounce-MX record set that remains stable when Sequenzy changes the
underlying delivery transport. The setup also requires one valid DMARC record;
the suggested `v=DMARC1; p=none;` value is a safe monitoring policy, and an
existing valid policy at the organizational domain is accepted.
Existing sending domains keep their current Amazon SES DNS records and are not
migrated automatically. API and agent integrations should publish every record
returned by domain creation instead of assuming a fixed provider or record
count.
When an operator later attaches a unified domain to Sequenzy's MTA, customer
mail stays blocked from that route until a hidden transactional probe receives
a correlated delivery receipt. Failed or missing receipts remain visible to
operators and can be retried without changing customer DNS.
[View domain verification documentation](/guides/domain-verification)
### Safe Step Deletion in Live Sequences
Deleting a sequence step no longer strands subscribers who are currently in
it. Anyone waiting inside a deleted step (a delay, a wait-for-event, or an
email step) now moves to the next step immediately and continues through the
sequence, matching the rewired flow new enrollees follow. The editor warns
you before the delete when people are in the step, and the sequence canvas
now shows a notice if any subscribers are stuck on a step that was removed
in the past. The same behavior applies to sequence edits made through the
API and MCP, which report how many recipients were moved. Existing deletion
protections still apply to trigger and end steps and to active or generating
A/B tests. For otherwise-deletable steps, recipient migration is refused when
people are in a branch step and multiple paths survive, since there is no
single next step to move them to.
[View sequences documentation](/concepts/sequences)
### Account-wide API Keys
Account Settings now has an API Keys page for creating and revoking personal
`seq_user_` keys. One account key can work across every workspace the user can
access, including newly created workspaces; API requests select the target with
the `x-company-id` header. Permission presets still apply, so multi-workspace
automation can be limited to read-only, data-ingest, transactional, safer-agent,
or full access. Viewer memberships remain read-only even if the account key has
broader permissions.
[View authentication documentation](/authentication)
### Reliable Cart Abandonment Trigger
Cart recovery no longer depends on shoppers visiting the cart page. Sequenzy
now maintains a live cart snapshot for every known shopper from add-to-cart,
remove-from-cart, and cart-view activity (Shopify web pixel and the
WooCommerce plugin). Each cart change restarts an inactivity timer; once the
cart sits untouched for the configured window (1 hour by default) with no
checkout or order, the new **Cart Abandoned** trigger
(`ecommerce.cart_abandoned`) fires with the full cart - every line item with
title, variant, quantity, price, image, and product URL - so recovery emails
always show the complete cart, not just the last product added. Starting a
checkout or placing an order cancels the pending trigger, and a per-shopper
cooldown prevents repeat sends. Timing and cooldown are configurable per
store in the Shopify integration settings and via the API
(`/api/v1/shopify/automation-settings`), CLI (`sequenzy shopify settings`),
and MCP. The Abandoned Cart Recovery sequence template now uses the new
trigger, and the legacy `ecommerce.cart_started` event (which only fired on
cart-page views) keeps working for existing automations but is no longer
offered for new ones.
[View documentation](/concepts/events)
### Direct Sequence Step Test Sends for Agents
Agents can now send any saved sequence email step straight to one or more
internal reviewers without copying it into a temporary campaign. The MCP
`send_sequence_test_email` tool accepts a sequence ID, email-step node ID, and
up to ten reviewer addresses. The REST API and `sequenzy sequences test` CLI
command expose the same workflow, returning a durable email send ID for each
recipient so delivery problems can be inspected directly.
[View documentation](/api-reference/sequences/send-test)
### Send API Engagement Metrics
Send API open and click rates are now available through the account metrics API
with `emailType=transactional`, the CLI with
`sequenzy stats --email-type transactional`, and the MCP `get_stats` tool with
`emailType: "transactional"`. This includes direct and saved-template sends;
the returned `emailSendId` still provides the delivery-level status and event
timeline through the email-sends API, CLI, and MCP tool.
[View documentation](/api-reference/analytics/metrics)
### Searchable Delivery History and Per-Email Stats
The recent sent-email history is now queryable by subject/title, recipient,
delivery status, email type, bounce type, date window, and source across the
REST API, CLI, and MCP. Returned delivery IDs drill into exact open/click
timestamps and events. Saved transactional emails also have dedicated
aggregate metrics by ID or slug, with optional time windows.
Transactional template lists can now search name, slug, or subject, filter
active state, and sort by sends, opens, clicks, open rate, or CTR across REST,
CLI, and MCP. Per-template stats now include top clicked links, complaints,
replies, permanent/transient bounce details, and human-versus-machine
engagement. The CLI can export the complete filtered delivery history with
`email-sends list --all --csv email-sends.csv`, and CLI/MCP delivery results
include direct dashboard URLs.
[List sent emails](/api-reference/email-sends/list) · [View transactional email metrics](/api-reference/analytics/transactional-metrics)
### Scheduled Campaigns Can Return to Draft
Scheduled campaigns can now be unscheduled without permanently cancelling and
recreating them. The dashboard, public API, CLI (`campaigns unschedule`), and
MCP (`unschedule_campaign`) all remove the pending send and recurrence, return
the campaign to an editable draft, and preserve its content and audience.
[View documentation](/api-reference/campaigns/unschedule)
### Subscriber Names in Transactional API Sends
Single-recipient transactional sends now use the matched subscriber's saved
first and last name when the API request omits those variables. Matching works
with `subscriberExternalId` or the recipient email, and explicit request
variables still take precedence. Multi-recipient sends remain shared messages
and do not borrow one subscriber's profile.
[View documentation](/api-reference/transactional/send)
### Per-Enrollment Sequence Attachments
Event-triggered sequences can now attach a different file for every enrollment.
In an email step, choose **From trigger** and use an event URL such as
`{{event.file_url}}`; filenames can also use event values, for example
`file-{{event.order_id}}.pdf`. Sequenzy resolves values from the original
enrollment event, validates the final public URL and filename, and fetches the
file when the email sends. The same `{ filename, path }` templates work through
the sequences API, CLI, and MCP tools.
[View documentation](/concepts/sequences#file-attachments)
### Per-Customer Predictive Analytics
Every customer with order history now gets individual predictions computed
nightly from your store's buying patterns: predicted spend over the next 12
months (`predictedLtv`), churn risk (`churnRisk`), expected next order date
(`expectedNextOrderAt`), and typical order cadence (`avgDaysBetweenOrders`).
Predictions are stored as ordinary subscriber attributes, so they work
everywhere attributes do - segment filters (including relative dates like
"expected to reorder in the next 7 days"), campaign targeting, subscriber API
and CLI responses, and the MCP `get_subscriber` tool - and each subscriber's
profile shows a Predictions panel with confidence and freshness. Three
predefined segments ship with it: At-risk customers, Predicted VIPs, and Due
to reorder soon. Two new automation events, `ecommerce.reorder_window_missed`
and `ecommerce.churn_risk_elevated`, fire once per order cycle so winback and
at-risk sequences trigger on each customer's actual buying cadence instead of
a fixed delay. Works with Shopify, WooCommerce, and Commerce API order
sources. [View documentation](/concepts/predictive-analytics)
### Product Recommendations: First-Class Block, Ranking Strategies, and Revenue Attribution
Personalized product recommendations are now a first-class part of Sequenzy.
A new **Recommended products** block in the email editor's Commerce section
drops in a ready-made product list that picks different products for every
recipient at send time - no configuration required.
You can now choose how products are ranked: **Personalized for each
recipient** (browsing activity first, then similar shoppers, then
store-popular products), **Best sellers** by recent order volume, **Newest
products**, or **Recently viewed**. Activity-based strategies accept a
configurable activity window.
Recommendations now report their own funnel. Campaign and sequence stats show
how many recommended products were shown and clicked, plus attributed orders
and revenue - an order counts when a subscriber buys a recommended product
within 7 days of clicking it - along with a per-product breakdown. The same
time-range-aware numbers are available from the API (`recommendations` on
campaign and sequence metrics), the CLI, and the MCP `get_campaign_stats` and
`get_sequence_stats` tools. Revenue totals stay separated by order currency.
[View documentation](/concepts/product-recommendations)
The dashboard now includes an "Ask Your Agent" section with copy-ready
prompts - like analyzing customer LTV or building high-ROI sequences - that
you can paste into any AI agent connected to Sequenzy via MCP, the CLI, or
the API.
Plain emails are now truly minimal - text, one clear call to action, and
at most a discount code on offer emails - across AI generation and the
Classic campaign template gallery, while designed styles keep their rich
layouts, making the two clearly distinct.
### Visibility for Skipped Sequence Enrollments
When an unsubscribed or bounced contact triggers an event that matches an
active sequence, the skip is no longer silent. The dashboard activity feed
marks the contact and shows "Would trigger if subscribed" on the matching
sequence instead of "Triggered", the sequence trigger step shows a skipped
count, and the contact's activity timeline records an "Enrollment skipped"
entry with the reason. Sequence stats also expose the skip counts through the
API (`enrollmentSkipped` on `GET /api/v1/metrics/sequences/{sequenceId}`), the
CLI (`stats --sequence`), and the `get_sequence_stats` MCP tool.
Deleting a user in a connected auth provider (Supabase, Clerk) unsubscribes
the contact, and that specific unsubscribe is now reversible by the same
provider: re-creating the user reactivates the contact automatically. Explicit
opt-outs (unsubscribe link, dashboard, API) are never undone.
[View documentation](/integrations/supabase#user-lifecycle)
Dashboard pages now show content-shaped loading skeletons instead of
spinners, so navigating between campaigns, sequences, subscribers, and other
screens feels smoother and less jumpy.
### Dynamic Carts in Generated Recovery Emails
AI-generated abandoned cart and abandoned checkout sequences now render each
recipient's actual cart. Generated emails include a dynamic item list bound to
the trigger event's line items (product image, title, quantity, and formatted
price resolve per recipient at send time), and checkout recovery CTAs link to the
recipient's personal resume-checkout URL when the commerce source supplies
one, with the brand's cart page as a safe fallback. This applies to the
prebuilt Abandoned Cart and Abandoned Checkout templates and to any cart or
checkout event-triggered sequence generated via the dashboard, API, CLI, or
MCP. Consecutive recovery steps now rotate through stable, recovery-specific
item layouts instead of repeating the same card treatment; visual emails use
card-row, editorial-row, product-grid, and receipt-row presets, while plain
emails use restrained thumbnail, receipt, and subtle-card variants. Checkout
events use a consistent optional `checkoutUrl` contract, and
order events sync a `lastOrderAt` subscriber attribute for recency-based
personalization.
The sequence editor now shows a persistent chip on the canvas whenever a
sequence is not active, with a one-click Activate action - so a freshly
generated draft is never mistaken for a live automation.
### Predictive Commerce Analytics
Shopify order history and live commerce orders now power explainable predicted
AOV, predicted 12-month customer value, and a 90-day order and revenue
forecast. The Metrics dashboard shows confidence-aware ranges and reports the
exact data still needed instead of estimating from sparse history. The same
forecast is included in `GET /api/v1/metrics`, `sequenzy stats`, and the MCP
`get_stats` tool. Forecasts use provider-neutral commerce order events, so the
model also works with WooCommerce and Commerce API orders.
[View analytics API documentation](/api-reference/analytics/metrics)
### File Attachments on Sequence Email Steps
Sequence email steps can now include real file attachments - ideal for lead
magnets and guides delivered by event-triggered sequences. Attachments are
URL-backed: provide a public HTTPS URL and a filename, and Sequenzy fetches
the file at send time and attaches it to the email, so you can update the file
at its source without editing the sequence. In the dashboard, upload a file
from the email editor's settings (gear) menu - Sequenzy hosts the upload for
you. You can also set `attachments` on
email steps through the sequences API, the CLI's step JSON, and the
`create_sequence` / `update_sequence` / `update_sequence_node` /
`insert_sequence_step` MCP tools. Up to 10 attachments and 7MB total per
email. Most email platforms don't support attachments in automated flows at
all - for broad marketing sends, a hosted download link is still the
deliverability-friendly default.
[View documentation](/concepts/sequences#file-attachments)
### Email Theme in API, CLI, and MCP
The company default email theme - preset, colors, typography, and layout - can
now be updated through the public API, CLI, and MCP server, not just the
dashboard. `PATCH /api/v1/companies/{id}` accepts an `emailTheme` object,
`companies update` gains `--email-theme-json` / `--email-theme-file`, and the
`update_company` MCP tool accepts the same field. Updates are partial: omitted
fields keep their current value, numeric values are clamped to supported
ranges, and passing `null` resets the theme to the platform default.
[View documentation](/api-reference/companies/update)
The email preferences page now shows a clear Unsubscribe button (and a
Resubscribe option after unsubscribing) when a company has no public
subscriber lists, instead of an empty page with no way to opt out.
Saved templates can now have their inbox preview text updated through REST,
the `sequenzy templates update --preview-text` CLI option, or MCP's
`update_template` tool without opening the dashboard.
The default footer text and link color in sent emails is now darker
(`#6b7280` instead of `#9ca3af`) so unsubscribe links and the subscription
reason stay readable on the default white email background. Footer background
choices and custom colors are unchanged.
### Full Sequence Authoring Parity for API, CLI, and MCP
`insert_sequence_step` can now create `logic_wait_for_event` and
`logic_branch` nodes with typed arguments. Branch paths can atomically wire to
existing node IDs - including the sequence completion node - with
`targetNodeId` and `elseTargetNodeId`, while still supporting newly created
path steps. This makes flows such as “reply in this sequence → complete,
otherwise → send the follow-up” possible without creating a dashboard node and
then replacing the graph in a second call. The REST API and CLI branch JSON use
the same target fields, and target-aware writes reject cycles or disconnected
flows before anything is saved.
The same public surfaces now cover every dashboard sequence setting: inbound
webhook triggers, descriptions, labels, recipient cancellation, typed stop
condition matching, segment-exit behavior, sequence and per-step delivery
identity, transactional emails, and CC/BCC recipients. Typed insertion also
covers delays, conditions, tag/list actions, subscriber updates, discounts,
outbound webhooks, SMS, wait nodes, and branches. Sequence email nodes can be
converted into A/B tests with independent variants, and sequence-level
duplicate, archive, and restore actions are available through REST, CLI, and
MCP.
Sequence authoring can now also start from a blank dashboard-compatible draft,
atomically replace trigger types, configure and rotate inbound-webhook
endpoints, manage persisted event or subscriber-attribute conversion goals,
filter the sequence list by status/search/labels, and read the user attribution
for enrollment pauses. REST, CLI, and MCP expose the same typed contracts.
API keys can now be safely cleaned up after a failed secret handoff. The public
API, CLI, and MCP server can list company keys as non-secret metadata and revoke
an exact key by ID. `list_api_keys` never returns the plain key or stored hash;
`revoke_api_key` is canonical and `delete_api_key` remains available as an
alias. The CLI provides matching `api-keys list` and confirmed `api-keys
revoke` / `delete` commands.
Workspace settings now open on the Domain tab, and the old Product Info page is split into focused tabs: Product Info (name, logo, social links, legal details), AI Context (tone, company context, value props, testimonials, pricing tiers), and Email Design (default email theme). The primary email language and text direction now live on the Localization tab alongside translations.
Event pickers in sequence triggers, goals, and subscriber filters now sort events by how often they fired in the last 24 hours and show the count next to each event. Event pickers also only offer the built-in events that match your business - SaaS lifecycle events are hidden for ecommerce stores and ecommerce events are hidden for SaaS products.
Revenue attribution is now more accurate: clicks always beat opens for last-touch credit (as documented), automatic opens from privacy proxies and security scanners no longer earn attribution, and goals with attribution windows longer than 24 hours now report every conversion recorded inside their configured window.
Sequence email Format updates through the API, CLI, and MCP now work for native
block emails that contain supported custom HTML blocks, such as an HTML footer
component. Only emails stored entirely as standalone raw HTML remain excluded.
The PostHog integration now updates subscriber attributes only for `$identify` events. Identify profile data (`$set`, `$set_once`, and person properties) is synced, while all other event properties stay on the event and are no longer copied onto the subscriber profile.
### Sequence Email Format in API, CLI, and MCP
Sequence reads now return each linked email's effective `emailPreset`, matching
the dashboard's **Style > Format** setting. API, CLI, and MCP updates can set a
native-block email to `minimal` or `branded` without changing the company theme,
using the same block transformation as the dashboard. Raw HTML emails are left
unchanged and do not support format updates.
"Start from scratch" in campaigns, sequences, and transactional emails now expands into an AI prompt with suggestion chips - describe what you want and land in the editor with the draft already generating, or keep using the plain empty editor via the link below the prompt.
### Editorial Block Styles: New Buttons, Pull Quotes, Captions, and More
A set of editorial upgrades across the core email blocks, so emails can feel
designed without leaving the block editor:
* **Two new button styles**: **Ink**, a near-black fill that pairs with any
brand color, and **Link**, a quiet letterspaced-caps text link with a
trailing arrow for secondary actions.
* **Editorial testimonial**: a large serif pull quote set between thin
hairline rules, alongside the existing card, minimal, centered, and quote
layouts.
* **Image captions and frames**: add a small muted caption under any photo,
and frame it with a hairline border or a white gallery mat.
* **Numbered headings**: give a heading an oversized index like "01" for
numbered editorial sections.
* **Ready-made editorial components in the block menu**: Framed Box,
Numbered Heading, Editorial Quote, Ledger table, Matted Photo, the new
button styles, and dots/accent dividers are all one click (or one `/`
command) away.
Everything renders identically in the editor, the preview, and the sent
email, and stays Gmail and Outlook compatible. The AI email writer can use
all of the new styles too, and the designed template gallery already puts
them to work - museum-mat product shots, serif pull quotes, ink buttons,
and numbered bulletins across the collections. Every designed template now
uses typographic numbered steps and clean detail ledgers instead of number
circles and tinted tiles, so the whole gallery shares one editorial
language. Three new templates are
built entirely around the editorial components: **The Gallery Wall**
(product photography hung like framed prints), **The Index** (a numbered
table-of-contents newsletter), and **The Postcard** (a matted photo and a
handwritten-feel note). [View documentation](/concepts/visual-blocks)
### Visual Template Gallery with Live Previews
The Designed campaign template gallery now shows a real rendered preview of
every template - each card displays the actual email with your brand color,
logo, and imagery applied, so you pick by look instead of by name. The
designed templates themselves were also reworked with editorial typography,
larger photography, and warmer layouts across all twelve collections.
### Subscriber Contact Changes in the Activity Timeline
The subscriber activity timeline now records a "Contact updated" entry whenever a subscriber's first or last name changes, showing the old and new value and which write path (API, dashboard, automation, import, or sync) made the change.
Campaign sending and scheduling now validate content and audience server-side on every surface (dashboard, API, MCP): an empty subject, an email with no content blocks, or an audience with no include rules is rejected, and a filtered audience with an empty filter set resolves to zero recipients instead of the entire active list.
Popup and form builder improvements: the template galleries doubled to eight redesigned popup templates (including exit intent, live demo, announcement bar, and fullscreen welcome) and eight form templates (including demo request, email course, blog banner, and floating bar), template previews now show your company logo, fields can use full or half-width responsive layouts and show or hide their labels, countdown blocks get a date picker, popup success screens show only your success content, and popups with a side image now stack to a single column on mobile.
The template galleries also grew to twelve forms and twelve popups, and every template now has its own typography and shape - serif, monospaced, or sans headlines, sharp or generously rounded corners, and compact to spacious layouts - while all of them share your workspace brand colors, the same way landing page templates differ without changing your palette. New templates include a monospaced dev changelog, a serif Sunday essay, a sharp-cornered studio form, an extra-rounded club signup, a monospaced launch modal, a serif slide-in digest, a stripped-back fullscreen takeover, and a slim top-of-page announcement bar.
Also for forms and popups: field settings gained a "Saves to" picker so a field can write to the subscriber's first name, last name, or phone (or to a custom attribute with your own key) instead of everything landing in custom attributes, and the confirmation page shown after a native HTML form post now mirrors your form's success screen with its checkmark, heading, and message. Applying a saved form to a landing page now carries every field over - phone maps onto the subscriber profile, and choice, consent, and hidden fields become native landing-page form fields instead of being skipped.
The MCP `add_subscriber` and `update_subscriber` tools now accept native `firstName` and `lastName` fields, and `sequenzy subscribers update` gained matching `--first-name` / `--last-name` flags, so agents no longer need to store names as custom attributes.
### One Canonical Way to Create Sequences
`create_sequence`, `POST /api/v1/sequences`, and
`sequenzy sequences create` are now the canonical sequence-creation surfaces.
The older `generate_sequence`, `POST /api/v1/generate/sequence`, and
`sequenzy generate sequence` names are deprecated compatibility aliases. They
now persist the same disabled sequence draft instead of returning unsaved
preview content, and their responses or command output include migration
guidance. Existing aliases remain available so integrations do not lose their
route or command unexpectedly.
### CC and BCC Recipients on Every Email
You can now add CC and BCC addresses to campaigns, individual sequence steps,
and transactional templates. In any email editor, click the **CC/BCC** arrow
next to the Reply field to reveal the recipient inputs - they stay hidden
until you use them. Each configured address (up to 10 per list) receives a
copy of every email sent, which is useful for compliance archives, CRM
ingestion, or keeping a teammate in the loop. The recipient themselves is
never copied, and addresses duplicated between CC and BCC are sent only once.
Sequence steps combine their own BCC list with the sequence-level BCC setting,
and transactional templates merge their saved CC/BCC with any `cc`/`bcc`
passed on the API request. Campaign CC/BCC can also be set via the API and
MCP with the `ccEmails` and `bccEmails` fields on campaign update.
### Static-Site Signup Forms and Forms MCP Tools
Saved Forms now have a dedicated **Static site / Form action** embed option.
Copy the public action URL, a minimal native form, or a fetch enhancement for
Astro, Hugo, Jekyll, Cloudflare Pages, Netlify, and GitHub Pages. These embeds
need no backend proxy or browser-side API key: the saved form keeps its lists,
tags, duplicate handling, and success action on the server. The hosted
JavaScript embed is also available to every workspace user instead of being
limited to global admins.
MCP agents can now use `list_forms`, `create_form`, and `get_form_embed` to
configure a list-scoped form and return working embed code. Integration-guide
requests for static frameworks use this Forms workflow and never recommend a
secret Bearer key in client code. [View signup form
documentation](/widgets/signup-form)
### Two-Way Per-List Consent Webhooks
Keep application-owned communication preferences in sync without polling.
Outbound webhooks can now subscribe to `subscriber.list_subscribed` and
`subscriber.list_unsubscribed`. Each signed event includes the event ID and
timestamp, subscriber ID, email and external ID, list ID and name, whether the
subscriber was added or removed, and the source of the change.
Events cover actual transitions made from subscription preferences, the
dashboard, API and MCP operations, automations, imports, double opt-in, and
integration-created memberships. Repeating an already-applied change does not
emit a duplicate transition. Global opt-outs continue to use
`subscriber.unsubscribed`. [View outbound webhook documentation](/integrations/outbound-webhooks)
### Segments, Events, and More in Conditional Blocks
Conditional (if/else) blocks and per-block **Conditional Display** rules can
now target much more than attributes and merge variables. You can show or hide
content based on segment membership, custom events (for example "performed
`purchase` in the last 30 days"), tags, lists, subscription status, engagement
(opened or clicked recently), Stripe and store purchases, signup date, email
provider, phone presence, and SMS status - the same filters you already use to
build segments.
Each rule is evaluated against live subscriber data at send time, so one email
can greet VIP customers with a different offer than everyone else. Recipients
that cannot be matched (for example a transactional recipient who is not a
subscriber yet) see the OTHERWISE branch. The editor preview shows the IF
branch by default; use the branch flip control to check the other one.
[View documentation](/concepts/campaigns)
### Programmatic Template Localization
Localized template variants can now be managed without opening the dashboard.
The REST API, CLI, and MCP can all store caller-supplied translations or queue
Sequenzy's AI translation for selected locales. Manual variants accept either
raw HTML or native Sequenzy blocks, preserve canonical locale handling, and are
immediately available to locale-aware campaign, sequence, and transactional
sends. Explicit sync works even when automatic on-save translation is disabled.
[View localization documentation](/concepts/email-localization)
### See Which Links Got Clicked in a Campaign
Campaign analytics now show a **Clicked Links** breakdown for sent campaigns:
the top tracked links in the email, ranked by clicks, with each link's share
of campaign-wide clicks. When the originating element can be resolved, the row
shows the button text, image, or text link in your email, with the destination
URL underneath; otherwise it shows the destination URL. When several links
share a destination, the row names all of them. Click any row to open the
destination. Find it in the campaign analytics view switcher, next to the
Sent Emails, Globe, and Engagement Over Time views.
The breakdown is also available outside the dashboard: the
[campaign metrics API](/api-reference/analytics/campaign-metrics) returns a
top-level `clickedLinks` array, `sequenzy stats --campaign ` prints a
Clicked Links section in the CLI, and the MCP `get_campaign_stats` tool
includes the same array for agents.
### More Accurate Click Counts
Click tracking now only wraps real links (anchors, image maps, and Outlook
button fallbacks). Previously, font stylesheet references in the email head
could be routed through click tracking, so email clients fetching fonts while
rendering registered phantom clicks. Font CDN fetches are also classified as
non-human engagement retroactively, so click counts and click rates for
past campaigns clean up automatically.
### Upload Email Images Directly from MCP
MCP agents can now upload screenshots and other email images without opening
the dashboard. `upload_image_asset` accepts a local image path from stdio MCP
clients or base64 bytes from remote-capable clients, saves the file in the
workspace's shared media library, and returns both the hosted URL and a
ready-to-insert image block. Alt text, responsive width, alignment, intrinsic
dimensions, and fixed-height `cover`/`contain` crops can be set in the same
call, so lifecycle screenshots use one precise treatment across editor preview
and sent email. PNG, JPEG, GIF, and WebP images up to 5MB are supported.
[View MCP documentation](/concepts/mcp)
### Custom Email Locales
You can now use locale codes beyond the built-in language picker for both your
company's primary email language and additional localized variants. Enter a
standard locale such as `fr-CA`, `sr-Latn`, or `en-AU`; Sequenzy validates and
normalizes the code, uses its language in AI writing and translation prompts,
and keeps existing variants stored under deprecated aliases such as `iw`
available under their modern locale (`he`). The language picker also includes
more ready-to-select languages, including Amharic, Bengali, Bulgarian,
Croatian, Persian, Serbian, Swahili, and Tamil.
[View documentation](/concepts/email-localization)
### Agent Handoff Prompts Now Include Setup Instructions
Prompts from the **Open in** menu in the sequence builder and email editor
sidebars are now self-sufficient. If the agent you hand off to is not
connected to Sequenzy yet, the prompt itself tells it how to get access:
add the remote MCP server in the agent's connector settings (with per-client
setup guides for ChatGPT, Claude, and Cursor), or use an API key from
**Settings → API Keys** against the REST API. The prompt also names your
workspace so the agent picks the right company. The MCP docs now include a
ChatGPT connector setup guide as well.
[View documentation](/concepts/mcp)
### Edit Any Existing Sequence Node from MCP
MCP agents can now patch existing sequence nodes in place instead of rebuilding
a flow. `update_sequence_node` handles one focused edit, while
`update_sequence_nodes` applies a batch atomically - including the common case
of changing every 5-minute delay to 7 days. Both tools work across stored node
types, including delays, email and SMS content, actions, conditions, webhooks,
branch configuration, and triggers. `get_sequence` supplies each node's current
config and `updatedAt` value plus `updateHints` with its editable and managed
fields for type-aware, concurrency-safe edits.
Node type conversion and topology remain explicit graph operations through
`edit_sequence_graph`. Active-sequence node edits require confirmation, and
recipients already waiting keep their previously scheduled time. [View MCP
documentation](/concepts/mcp)
### Polls & NPS Surveys in Emails
Ask a question, get one-click answers. The new **Poll** block renders answer buttons (up to 8, emoji welcome) and the **NPS Survey** block renders a 0-10 recommendation scale - both look identical in the editor, the preview, and the sent email, and work in every email client because each answer is a plain link.
* **Feeds segmentation directly**: every answer is saved to a subscriber attribute you choose (NPS scores as numbers, so segments can filter e.g. `nps_score` at most 6 for detractors) and appears in the subscriber's activity feed.
* **Automate follow-ups**: each vote fires a `poll.answered` event you can use in Event Received sequence triggers and sync rules - thank promoters, win back detractors.
* **Bot-proof**: clicks from security scanners and link-preview bots are filtered out, and re-votes simply update the stored answer.
* **After voting**: recipients land on a hosted thank-you page with your custom message (previewed live in the block settings), or redirect them to any URL.
* **Eight visual styles**: Soft, Pill, Outline, and Filled buttons, plus four designed looks - Pop (hard-shadow boxes), Brutal (square uppercase slabs with ink shadows), Quiz (A/B/C letter badges), and Minimal (a bare ruled list with accent arrows). The style picker previews each one in your accent color.
* **Results where you expect them**: sent campaigns show a Poll Results card with the answer distribution, and NPS surveys get the score with the promoter / passive / detractor split. Click any answer row - or the promoter / passive / detractor counts - to open the exact historical subscribers behind that campaign and poll block, even if their current stored answer later changes. The same summary is returned by the campaign stats API, CLI, and MCP as a `polls` array; MCP can recreate the drill-down with the campaign-and-block-scoped `pollResponse` segment filter.
* **Tamper-proof and deduplicated**: every answer link is cryptographically signed, and re-clicks of the same answer never re-fire your automations.
* **Push answers anywhere**: subscribe an outbound webhook to the new `poll.answered` event.
Insert it from the block menu or type `/poll` or `/nps`. Poll blocks are also supported in emails created via the API, CLI, and MCP, and the AI email writer can generate them. The hosted thank-you page follows your company's email language. [View documentation](/concepts/polls)
### Custom Event Setup Returned With New Sequences
Creating a custom-event sequence through the API or MCP now returns everything needed to connect the application-owned event feed: ready-to-adapt tracking code, the exact event payload contract, matching-field properties, a generated example payload with an explicit filter-match status and adaptation note when needed, the event endpoint, a direct documentation link, and the arguments for the MCP integration-guide tool. The MCP result keeps this guidance after its sequence readback instead of dropping it.
### Per-Step Sender Identity for Sequence Emails via API, CLI, and MCP
Individual email steps in a sequence can now get their own sender identity through the API, CLI, and MCP - not just in the dashboard editor. Pass `senderProfileId` or `fromEmail` (plus optional `fromName`), and `replyProfileId` or `replyTo` (plus optional `replyToName`) on any item in the `emails`/`steps` array of the update sequence endpoint, or on email steps inserted with `insertSteps`. A `fromName` on its own changes only the visible display name without touching the sender profile.
New email steps inserted into an existing sequence also now inherit the effective sender identity of the nearest sequence email, so added steps send as the same person as their neighbors. After a branch merge, only identity fields shared by every incoming path are inherited; conflicting fields use the sequence or company defaults. [View documentation](/api-reference/sequences/update)
### Restructure Existing Sequence Graphs from MCP
Agents can now move, reorder, reconnect, delete, and duplicate existing
sequence nodes instead of only inserting new steps. The new
`edit_sequence_graph` MCP tool can move an A/B test below a branch convergence,
deep-copy an A/B test with independent variants and emails, splice out linear
steps, or atomically replace the full edge topology for advanced reconnects.
`get_sequence` now returns edges plus a graph revision, and every graph edit
checks that revision before writing so a stale agent cannot overwrite a newer
change. Active-recipient and topology safeguards prevent destructive edits from
stranding live runs. [View sequence update API](/api-reference/sequences/update)
### Segment by Number of Opens and Clicks
Engagement filters can now count, not just check. Pick "opened at least" or "clicked at least" (or their "less than" counterparts) on any engagement filter and enter a count with a time window - `10:30d` for 10+ times in the last 30 days, or `10:all` for 10+ times ever. This unlocks segments like "best clickers - clicked more than 10 times" or "opened 10+ emails but never clicked" for catching bot and spamtrap opens. The value picker suggests count-and-range combinations as you type, the AI filter builder understands phrases like "clicked more than 10 times", and the same `count:timeRange` values work in segments created via the API, CLI, and MCP. [View documentation](/concepts/subscribers)
### Real Event Data in Previews and Test Sends
Emails inside an event-triggered sequence now preview `{{event.*}}` merge tags with values from the most recent matching event, instead of showing fallbacks or blanks. Test sends from the sequence email editor include the same sample payload, so the email you receive matches what a live send would render. Every prefilled value stays editable in the preview data panel, and anything you type there wins over the sample. [View documentation](/concepts/events)
### Event Variables in Update Subscriber Steps
Sequence "Update Subscriber" steps can now set field values from the payload of the event that started the sequence. Type `{{` in any value field to autocomplete variables like `{{event.plan}}` or `{{event.amount}}`, drawn from the trigger event's recent payloads - subscriber attributes (`{{subscriber.company}}`) and built-in fields (`{{firstName}}`, `{{email}}`) are suggested too. Variables resolve when the step runs; number fields accept variables and coerce the resolved value, and a field whose event property is missing is skipped instead of being blanked. Works the same for sequences created via the API, CLI, and MCP.
### Affonso Affiliate Integration
Connect your [Affonso](https://affonso.io) affiliate program to Sequenzy and run your affiliate communications from the same place as the rest of your email.
* **Affiliates sync as subscribers** with their tracking code, status, program, coupon code, and any custom metadata from your signup form.
* **Existing affiliates and performance totals are backfilled on connect** with a read-only Affonso API key, without replaying historical automation events or enrolling them into sequences.
* **Running totals per affiliate**: leads referred, conversions, total referred revenue, commission earned, and total paid out are kept up to date as attributes, so you can segment affiliates by performance (for example, referred revenue over \$1,000). The read-only API key needs Affiliates, Referrals, and Commissions read permissions.
* **Automate the whole lifecycle** with provider-agnostic `affiliate.*` events: welcome new affiliates on `affiliate.created`, celebrate wins on `affiliate.referral_converted`, and confirm payments on `affiliate.payout_paid`, with event details like `{{event.amount}}` available in your emails. Attribute and event names are not tied to Affonso, so your automations and segments keep working if you ever switch affiliate platforms.
Connect it in Settings → Integrations → Affiliate Marketing, then paste an Affonso API key and the signing secret from Affonso's webhook settings. Every webhook is signature-verified. [View documentation](/integrations/affonso)
### Designed Template Gallery
The campaign template picker now has a second, visual-first gallery: 88
designed templates built on the visual block set - photo heroes, lookbooks,
magazine-style digests, pricing cards, countdown sales, polls, and more,
organized into twelve categories from Welcome to Product News. The classic
functional templates stay the default; flip the new Classic/Designed switch
in the picker to browse them.
Four of the categories are wilder by design. Statement templates are
typographic and poster-like (a manifesto, a brutalist bulletin, three
stacked headline bands, a one-word email). Playful ones behave like games
(subscriber bingo, a pop quiz, a "do not open" mystery, a \$0.00 receipt
from the future). Cinematic ones read like film scenes (a heist briefing, a
midnight drop, a museum label, a mission launch). Avant-Garde ones borrow
formats from other mediums entirely (a boarding pass, a tasting menu, a
founder interview, a transmission from 2049).
Designed templates personalize themselves from your brand: your logo, brand
colors, harvested website imagery, synced products, and real customer
testimonials drop into the layouts automatically, with curated stock
photography as the fallback. Templates also carry distinct corner styling -
editorial layouts run square corners, playful ones extra-soft rounding - so
the gallery doesn't feel like one email repeated 56 times.
Template previews now resolve personalization fallbacks too, so
`{{FIRST_NAME|there}}` shows as "there" instead of a raw tag.
[View documentation](/concepts/visual-blocks)
### Prompt Creation Parity
Campaigns, saved templates, and transactional templates now accept prompts directly across the API, MCP, and CLI. Prompt creation uses the same branded native-block generator as the dashboard; campaigns remain drafts and prompt-created transactional templates default to disabled. Existing blocks and preserved HTML modes remain supported.
### Smoother Editing for Imported HTML Emails
A round of fixes that make the HTML editor (used for imported emails) behave
like a proper text editor:
* **Links are real text now**: click a link or button label to place the
cursor and type, double-click to select a word, and drag to select across
it. The link editor opens alongside without stealing your keyboard, and it
now includes a Text field so you can change a button's label and URL in
one place.
* **Undo and redo work reliably**: one Cmd/Ctrl+Z undoes your last edit and
keeps the cursor where you were, redo brings it back, and saving after an
undo saves what you see.
* **Cmd/Ctrl+K adds a link** using the same link editor as the rest of the
app.
* **Forward Delete respects your layout**: deleting at the end of a section
removes spacers or joins the next text section instead of breaking the
email's structure.
* **The view follows your cursor** when typing or pressing Enter near the
bottom of a long email.
* **Spell check is on** while writing, and the invisible characters some
editors leave behind no longer end up in sent emails.
### A More Fluid Email Editor
A batch of editing and drag-and-drop refinements that make composing emails
feel like a modern document editor:
* **Enter after a heading** starts a new paragraph below it (press Enter
mid-heading to split it), so you can type a title and keep writing.
* **Backspace works between blocks**: it deletes an empty block, and at the
very start of a block it joins the text into the previous one with the
cursor at the junction.
* **Drag and drop is precise**: dragged blocks track your cursor exactly,
drops land above or below a block depending on which half you release over
(with the insertion line showing exactly where), a new drop zone below the
last block lets you drop content at the end of the email, and dragging into
or out of columns shows the same insertion line.
* **Undo keeps your place**: after Cmd/Ctrl+Z the cursor returns to where you
were typing, so you can undo and keep writing without reaching for the
mouse.
* **Arrow keys cross blocks**: Left/Right at the edge of a block move into
the neighboring one, and Escape steps out of text editing to the selected
block, then clears the selection.
* **Slash commands clean up after themselves** - filtering the menu (like
`/div`) no longer leaves the typed query behind in the email - and the
formatting toolbar now waits until you finish selecting text instead of
popping in mid-selection.
### Dark Mode and Blocked-Images Preview
The editor's Preview tab can now simulate the two most common ways an email
breaks after sending. The dark mode toggle recolors your email the way Gmail
and Apple Mail force dark mode - light backgrounds go dark, dark text goes
light, and saturated brand colors stay put - so invisible text and
light-background logos show up before your subscribers see them. The
blocked-images toggle renders the email the way Outlook and corporate inboxes
show it before images load, so you can check that your alt text carries the
message.
### Smarter Pre-Send Checks
The deliverability checker now also catches practical mistakes, not just spam
signals: buttons and CTAs with no link or a placeholder link ("#", a bare
"https\://"), images missing alt text, button text with unreadable contrast,
buttons too small to tap on mobile, and emails whose HTML exceeds Gmail's
102KB clipping limit (which hides the rest of the message - including the
unsubscribe link - behind "View entire message").
### Sequence Editor: Undo, Simulation, and More
The sequence editor got a major usability upgrade:
* **Undo and redo.** Every canvas edit - adding, editing, deleting, and
reordering steps - can be undone with Cmd+Z and redone with Cmd+Shift+Z,
including accidental "Discard changes" clicks.
* **Path simulation.** Pick any subscriber and see exactly which steps they
would take through the sequence, evaluated with the same condition logic
the send path uses. The canvas highlights the path and explains each
condition and branch decision. Nothing is sent or enrolled.
* **Funnel view.** Toggle the new funnel mode (F) on an active sequence to
see where it leaks. Connections are only annotated where subscribers were
actually lost (hover for the breakdown of failed, stopped, or finished),
branch splits show their traffic share, and each email shows its open-rate
change versus the previous one - the usual drop-off signal in a linear
sequence.
* **Duplicate step.** Any action or delay step can be duplicated in place
from its editor. Duplicating an email step also clones the email, so the
copy is independently editable after you save the sequence.
* **Collapsible branches.** Wide branch sections can be collapsed into a
compact summary card showing how many steps are hidden inside.
* **Keyboard navigation.** Arrow keys walk the canvas, Enter opens the
focused email step, Delete removes it, and Cmd+F opens a step finder that
jumps to any step by name or subject.
* **Sort sequences by results.** Sequence lists can now be sorted by goal
conversions and attributed revenue, alongside date, opens, clicks, and CTR.
[View documentation](/concepts/sequences)
### Company Branding for AI-Generated Emails
Emails generated through the API, MCP server, and CLI now match drafts created
in the dashboard. Generated marketing emails include the company's logo and
footer by default, transactional emails use a footer without an unsubscribe
link, and campaigns and templates created through the API inherit the company
font. API and MCP callers can set `applyBranding: false` when they need only the
raw generated content blocks.
[View documentation](/api-reference/generate/email)
### Continue in Your Agent from the AI Sidebars
The AI assistant sidebars in the sequence builder and the email editor now
have an **Open in** menu next to the message box. Draft your request, then
copy it as an agent-ready prompt or open it directly in ChatGPT, Claude, or
the Cursor desktop app.
The prompt includes your company and sequence or campaign identifiers, so an
agent connected through the Sequenzy MCP server can pick up exactly where you
left off. The menu also links straight to the AI Agent Setup wizard if you
have not connected an agent yet.
[View documentation](/concepts/mcp)
### Full Recipient List on Multi-Recipient Transactional Sends
Transactional emails sent to multiple `to` addresses (or with cc/bcc) now
store the complete recipient list on the send record. The sent-email detail
page shows every recipient - including cc and bcc - instead of only the first
To address, and the email-sends API returns the list in a new
`additionalRecipients` field.
[View documentation](/api-reference/email-sends/get)
### Nested Repeat Blocks
Repeat blocks in the email editor can now be nested, with the inner repeat
drawing from an array inside each parent item - point its source at the
parent's item alias, like `item.lineItems`, instead of a separate top-level
array. The editor suggests these parent-scoped sources when a repeat block
sits inside another one, previews render real nested rows (including the
Preview tab, test emails, and the transactional API code examples), and
merge-tag suggestions inside the inner template pick up the nested item's
fields. Works in campaigns, sequences, and transactional templates.
[View documentation](/concepts/transactional-emails#nested-repeats)
### Product Feedback from the API, CLI, and MCP Server
You (or your AI agent) can now send product feedback to the Sequenzy team
from wherever you work: `POST /api/v1/feedback`, `sequenzy feedback "..."` in
the CLI, or the `submit_feedback` MCP tool. AI agents connected through the
MCP server are instructed to report a gap whenever you asked for something
the tools do not support yet - so the workflows you actually need reach us
without you filing anything. Feedback lands directly with the team and is
never stored in your account data.
[View documentation](/api-reference/feedback/submit)
### Chargebee Integration
Connect your Chargebee site and turn billing events into email automations:
* **One-time setup**: enter your site name, an API key, and webhook basic auth
credentials - Sequenzy shows you the exact webhook URL to paste into
Chargebee
* **Full historical sync**: customers, subscriptions, and paid invoices import
automatically on connect, with MRR and LTV calculated for every subscriber
* **Real-time billing triggers**: payments, failed payments, refunds, trial
starts, trial-ending reminders, trial conversions, cancellations, pauses,
resumes, and reactivations all map to built-in `saas.*` events for
automations
* **Automatic tags**: `customer`, `trial`, `cancelled`, `churned`,
`saas.monthly`, and `saas.yearly` stay current as subscription statuses
change
* **Duplicate-safe**: Chargebee webhook retries and duplicate deliveries never
double-count revenue, and a manual **Sync Revenue** action reconciles
anytime
* Supports both Product Catalog 1.0 and 2.0 sites, with non-USD currencies
converted automatically
[View documentation](/integrations/chargebee)
### Shopify SMS Consent Sync
Phone numbers and SMS marketing consent collected by your Shopify store now
flow into Sequenzy automatically. The initial customer sync imports each
customer's phone and SMS opt-in state (consent source `shopify`, with
Shopify's own consent timestamp), and customer webhooks apply opt-ins and
opt-outs as they happen - so a checkout opt-in on your store is immediately
usable by Send SMS steps in your sequences. The sync is TCPA-safe: consent is
only granted on an explicit Shopify opt-in, never inferred from a phone
number. Requires approved phone field access for protected customer data on
your store; without it, customer sync simply skips phone and consent.
[View documentation](/concepts/sms#shopify-sms-consent-sync)
### One-Off SMS from the Subscriber Page
Text a single contact without building a sequence. When a subscriber is
opted in to SMS marketing, their profile page now shows a **Send SMS**
button next to **Send email**. It opens the familiar SMS composer - merge
tags, emoji, images, AI assist - shows the estimated credit cost for the
contact's country before you send, and warns if your credit balance is too
low. One-off messages respect all the usual consent gates and quiet hours.
[View documentation](/concepts/sms)
### Inline Emoji Search in the Email and SMS Editors
Type `:` at the start of a word in any text block of the email editor, or in
the SMS composer, to open an inline emoji searcher right at your cursor.
Keep typing to filter by shortcode (`:tada`, `:fire`, `:+1`), move through
the results with the arrow keys, and press Enter to insert - no need to
reach for the toolbar picker. A bare `:` shows the most frequently used
emoji, so a couple of keystrokes is usually all it takes.
In the SMS composer, the searcher also reminds you when inserting an emoji
will switch a standard GSM-7 message to Unicode segments (70 characters per
segment instead of 160), so the cost change is never a surprise.
### Sequence Builder: Components Palette and Organized Step Menu
Building sequences is faster and easier to scan:
* **Floating components palette**: a new collapsible panel on the left of the
sequence canvas lists every step type. Click a component to add it at the
end of your flow, or drag it onto the canvas - every insertion point
between steps expands into a labeled drop zone while you drag, and the one
under your cursor shows exactly what will be inserted
* **Organized "Add a step" menu**: step types are now grouped into Messages,
Timing, Logic, and Actions sections in a wider two-column layout everywhere
you add a step, so the right step is easier to find without scrolling
### SMS Everywhere: AI Assistant, API, CLI, and MCP
SMS is now a first-class citizen across every Sequenzy surface, not just the
sequence builder:
* **AI assistant adds SMS steps natively**: ask the sequence assistant to
"add a shipping confirmation text" and it proposes a real Send SMS step with
drafted copy - no more webhook workarounds. SMS additions always require
your explicit approval before they apply
* **AI SMS copywriting**: the new `POST /api/v1/generate/sms` endpoint (and
`sequenzy generate sms` in the CLI, `generate_sms` in MCP) drafts on-brand
message variants with live encoding and segment counts
* **SMS steps via API**: create sequences with SMS steps
(`type: "sms"` with plain-text `text`) in explicit steps, branch paths, and
step insertion - so agents and migrations can build SMS journeys end to end
* **SMS readiness check**: `GET /api/v1/sms/settings` (CLI:
`sequenzy sms settings`, MCP: `get_sms_settings`) reports add-on status,
credit balance, numbers, and whether SMS steps will actually send
* **Edit SMS steps programmatically**: update an existing SMS step's text,
label, or ineligible-contact behavior via `smsSteps` in
`PUT /api/v1/sequences/{id}` (MCP: `update_sequence.smsSteps`, CLI:
`sequenzy sequences update --sms-steps-json`)
* **Test sends from anywhere**: `POST /api/v1/sms/test` (CLI:
`sequenzy sms send-test`, MCP: `send_test_sms`) sends a real test message -
credit-charged, quiet-hours exempt, capped at 100 per company in a rolling
24-hour window
* **SMS webhook events**: outbound webhooks now emit `sms.sent`,
`sms.delivered`, `sms.failed`, and `sms.opted_out` (included in the default
event set for new webhooks), so external systems can track SMS delivery and
opt-outs in real time
* Compliance is automatic: Sequenzy adds your brand prefix to every message
and the opt-out footer to the first message, so generated copy never needs
boilerplate
[View documentation](/concepts/sms)
### BCC Copies for Sequences
Sequences can now blind-copy your team on every email they send:
* Set one or more **BCC addresses** in the sequence settings modal - for
example your customer support inbox, so the team sees exactly what each
subscriber receives (separate multiple addresses with commas, up to 10)
* Copies are sent as true BCCs, so subscribers never see the extra recipients
* Applies to every email step in the sequence, including A/B test variants
* Leave the field empty to turn it off
* Also available via the API (`bccEmails` on `PUT /api/v1/sequences/{id}`), the
CLI (`sequenzy sequences update --bcc-emails`), and the MCP `update_sequence`
tool
[View documentation](/concepts/sequences#bcc-copies)
### Best Emails on the Metrics Dashboard
The metrics page now shows your top performing emails, so you can see at a
glance which campaigns and sequence emails actually drive results:
* **Best emails leaderboard**: the top 3 emails for the selected period,
covering both one-off campaigns and individual sequence emails (like each
step of your onboarding flow)
* **Sort by what matters**: rank by attributed revenue, unique opens, or open
rate - revenue is the default whenever at least one email has attributed
revenue
* **Jump straight to the source**: each entry links to the campaign or
sequence behind it
* **Drill into the numbers**: click an entry's opens, open rate, or revenue to
see exactly which subscribers opened the email or made an attributed
purchase
### Finish Setup, Earn 500 Bonus Emails
Completing the getting-started checklist now earns you 500 bonus email
credits, added to your account automatically:
* The checklist has a new third step: **Generate an API key** - connect the
API, CLI, or your AI agent alongside creating your first campaign and adding
subscribers
* Finish all three steps and 500 bonus emails land on your account (one time
per account). Credits are spent before your plan allowance
* Any API key counts - one created from the setup flow, the settings page, or
a CLI/MCP login
### Meta Audience Syncs Handle Lost Connections Gracefully
Meta audience syncs now recover from connection problems instead of failing silently:
* **Self-healing audiences**: if the Meta audience behind a sync disappears (deleted in Ads Manager, or replaced by a reconnect), the next run recreates it automatically and continues syncing
* **Automatic pause on revoked connections**: when Meta stops recognizing the connection (password change, revoked permission), the sync is paused and marked **Disconnected** instead of retrying forever
* **Guided reconnect**: the audience sync dialog shows exactly what happened with a one-click reconnect button - reconnect, resume the sync, and it picks up on the next run
The `disconnected` status is also visible via the [audience syncs API](/api-reference/audience-syncs/list), CLI, and MCP. [View documentation](/integrations/meta-ads)
### International SMS With Cost-Weighted Credits
The SMS add-on now reaches 20+ countries beyond the US and Canada, including
the UK, Ireland, most of Western and Northern Europe, Poland, Czechia,
Ukraine, Australia, and New Zealand:
* **Cost-weighted credits**: US/CA segments stay at 1 credit; international
segments use a per-country multiplier that reflects real carrier cost, so
you're never surprised by destination pricing
* **Pricing calculator**: pick a destination country and a monthly budget in
`Settings -> SMS` to see how many messages it buys and the effective
per-SMS rate, plus a full "View all country rates" table
* **Country-aware quiet hours**: the 11am-8pm send window is now evaluated in
the recipient country's timezone, not just Eastern Time
* **Clear eligibility**: subscriber profiles show each contact's phone
country, the credit multiplier that applies, and exactly why a contact
can't receive SMS; unsupported countries are skipped, never silently billed
* SMS now requires a **paid plan** (from \$19/mo)
* **Add it at upgrade**: the plan upgrade dialog offers the SMS add-on under
Add-ons with a volume slider - pick a monthly credit bundle (1k for \$16/mo
up to 75k) and buy it together with your plan. It stays a monthly charge
even alongside an annual plan
* **Segment and branch on SMS reachability**: new `Phone Number` and
`SMS Status` segment filters, plus **Has phone number** and **Subscribed to
SMS** conditions in if/else and multi-branch sequence steps
* **Skip or exit for unreachable contacts**: each Send SMS step can either
skip contacts who can't receive SMS (default) or exit the sequence for them
* **At-a-glance consent in the subscriber list**: the Status column shows a
green phone icon when a contact has a number and a red one when they opted
out of SMS
[View documentation](/concepts/sms)
### Shipment Tracking, Browse Abandonment, and Price Drop Events for Shopify
The Shopify integration now covers the full post-purchase and browse journey:
* **Shipment events**: `ecommerce.order_shipped`, `ecommerce.order_out_for_delivery`, `ecommerce.order_delivered`, and `ecommerce.delivery_failed` fire per shipment with tracking number, tracking URL, and carrier - build shipping confirmations with tracking links, and time review requests off the actual delivery instead of the order date. Reconnect your store once to grant the new `read_fulfillments` scope.
* **Browse abandonment**: when a known subscriber views a product and doesn't buy, start a checkout, or add it to cart within the browse window (default 2 hours), `ecommerce.browse_abandoned` fires with the viewed product - at most once per subscriber per day
* **Price drop alerts**: when a product's price drops by 5% or more, `ecommerce.price_drop` fires for everyone who viewed or carted the product in the last 30 days, with old price, new price, and percent off. If no sequence handles the event, a default price-drop email goes out automatically (active subscribers only) - build your own sequence on the trigger to replace it
* **Accurate checkout recovery**: `ecommerce.checkout_started` now also fires from checkout updates the moment the buyer's email is known (once per checkout), and carries `abandonedCheckoutUrl` plus the final cart contents
* **Honest revenue on refunds and cancellations**: refunded and cancelled orders now subtract from subscriber revenue attributes (`totalSpent`, `ltv`, `aov`, `ordersCount`) and net out attributed campaign/sequence revenue
* **Live catalog**: product create/update/delete webhooks keep synced products, prices, and images fresh between full syncs
* **Richer browse tracking**: the storefront pixel now also captures collection views (`ecommerce.collection_viewed`) and searches (`ecommerce.search_submitted`)
* **Simple controls everywhere**: tune or disable browse abandonment and price drop from the Shopify settings dialog in the dashboard, `sequenzy shopify settings` in the CLI, the `get/update_shopify_automation_settings` MCP tools, or `GET/PUT /api/v1/shopify/automation-settings`
* **One-click sequence templates**: Shipping Confirmation, Delivered Review Request, Browse Abandonment, and Price Drop Alert templates, each pre-wired to the right trigger. Browse-abandonment and price-drop sequences automatically stop when the shopper buys the product
All new events are available as sequence triggers, sync rule triggers, and segment activity. [View documentation](/integrations/shopify)
### Evidence-Based Domain Verification
Domain verification now runs on a per-record evidence model, making statuses
trustworthy and actionable.
* **A new "Misconfigured" status.** When a DNS record exists but has the wrong
value (a conflicting MX from an old provider, a mistyped SPF), the domain now
shows "Misconfigured" with the exact record to fix - instead of a generic
"Failed". The API returns the new status as well.
* **No false alarms, structurally.** A domain status only changes after
repeated, confirmed observations spread over a time window. Lookup timeouts
and nameserver hiccups can never demote a domain; genuinely deleted records
are still caught within minutes of confirmation.
* **"Rechecking DNS records" hint.** If a check sees an inconsistent answer,
the domain stays verified and sending continues while Sequenzy re-confirms
over the next few minutes - and the dashboard says so.
* **Smarter setup window.** While you are actively adding records, the domain
stays "Pending" with live per-record feedback. Waiting on the email provider
never counts against your setup time.
* **Internal transition logs.** Every status transition is written to the
audit trail and logged in Telegram with the DNS/provider evidence that
triggered it.
* **Tracking domains** now follow the same verification model, including
internal status-change logs.
### Recurring Campaigns
Campaigns can now repeat on a schedule. Pick **Every week** or **Every month**
when scheduling a campaign and Sequenzy re-sends it automatically:
* The audience is re-evaluated at every run - new subscribers who match the
list or segment are included, unsubscribed or removed ones are skipped
* Each run appears as its own campaign with its own stats, named after the
template with the run date
* The email content is snapshotted per run, so you can keep editing the
template between sends without rewriting past run history
* Cancel the recurring campaign at any time to stop the series
Combined with product rotation (below), this powers set-and-forget monthly
digests - top picks for an interest segment - that never repeat products a
subscriber has already seen. [View documentation](/concepts/campaigns)
### Product Rotation: Don't Show The Same Products Every Send
Product lists in emails now rotate their content between sends, so each
recipient only sees products that are new to them:
* Recommended product lists rotate automatically - nothing to configure
* Catalog lists can opt in with the **Don't repeat products** toggle in the
block's settings
* Purchased products stay excluded, and when a recipient has seen everything,
the oldest-seen products come back so the list never renders empty
### Tag Buyers By What They Bought
Sync rules on commerce events can now match the purchased products. Add a
**Product match** condition to an Order Placed rule and tag buyers by product
tag, collection, or product type - for example, tag anyone who buys a product
tagged "Vinyl" as a `vinyl-collector`, then target that audience with its own
campaigns. [View documentation](/concepts/sync-rules)
Sync rules are also manageable programmatically now: `GET`/`PUT
/api/v1/sync-rules` on the API, `sequenzy sync-rules` on the CLI, and
`get_sync_rules` / `update_sync_rules` MCP tools. Recurring campaigns can be
scheduled programmatically too, via `recurringInterval` on the campaign
schedule endpoint, `--repeat` on the CLI, and `schedule_campaign` over MCP.
### Cleaner Block Settings In The Email Editor
Every block settings panel in the email editor has been redesigned around one
consistent structure, so the same controls always look and behave the same
way:
* Settings are now grouped into titled sections (Content, Layout, Style,
Visibility) with clear dividers, ordered by how often you need them
* Related controls share a row - width sliders include their unit toggle,
corner radius pairs a value input with quick presets, and padding uses four
clearly named inputs (Top, Right, Bottom, Left)
* Optional settings like fixed image height or discount expiration reveal
their controls only when switched on
* Container styling (background, border, radius, padding) folds into a
collapsed "Block Style" section that shows a one-line summary of what you
customized, so untouched defaults no longer crowd the panel
* The countdown deadline is now a single date-and-time picker instead of two
separate fields
* The panel is wider, so two-column rows have room to breathe
### Product Images As AI References For Everyone
Generating an image in the email editor can already use one of your product
photos as a reference, so the result stays faithful to your actual product.
Previously this was limited to e-commerce workspaces - now any company with
synced products (Shopify, WooCommerce, Stripe, or the products API) can pick a
product image as the reference.
We also polished the flow:
* The product reference picker now shows a visual grid of product photos, so
you pick by image instead of by name
* Image block settings show a live preview of the current image, and a new
**Generate with AI** shortcut opens the image dialog straight on the AI tab
### Dynamic Product Lists From Your Store
Repeat blocks can now pull products straight from your synced store catalog -
no merge variables needed. The block settings open with one question, **What
to show**, and each answer is a single choice:
* **Recommended products** - picked per recipient from their browsing and
cart activity, then similar shoppers, then store bestsellers, then the
newest in-stock products so the list never renders empty. The settings
panel explains exactly how picking works, and a product never repeats
within a list or across lists in the same email - even when the same
product was synced twice under different ids
* **Store catalog** - your catalog, sorted by newest, price, or title, with
optional filters by tag, collection, vendor, product type, or price range,
resolved fresh at send time
* **Hand-picked products** - select exactly which products to show and in
what order, with prices, images, and stock kept in sync with your store
* **Campaign list** - a personalized list computed by your campaign's rules
Recommended and catalog lists can also be fine-tuned per block:
* **Skip already-purchased products** leaves out anything the recipient
bought in the last year - on by default for recommendations, opt-in for
catalog lists
* **Match the trigger event's product** limits the list to products related
to the product on the event that triggered the email, so a
browse-abandonment sequence can recommend items related to what the shopper
viewed. The relation is customizable: pick which attributes must overlap
(tags, collection, product type, or brand) and whether products must match
any or all of them
Product lists work everywhere emails send: campaigns, **sequences**, A/B
tests, and test emails. Filters apply per send, so an abandoned-cart sequence
can always show in-stock products from a specific collection.
The editor previews product lists with real products from your catalog, and
new **item templates** (card grid, spotlight, compact rows, minimal list) let
you restyle a repeated list in one click.
Shopify product syncs now also capture tags, vendors, product types, and
collections, which power the new filters. Re-sync your store to pick them up.
[View documentation](/guides/email-editor)
### SMS & MMS Add-on
You can now send text messages to your US and Canadian subscribers with the new
SMS add-on - \$16/month including 1,000 message credits, with extra credit packs
available whenever you need more.
Enable it in **Settings -> SMS** and Sequenzy provisions a dedicated toll-free
number for you automatically. A short verification wizard collects your
business details, and once carriers approve the number (typically 1-5 business
days) you're ready to send.
Add a **Send SMS** step to any sequence and write the message in a lightweight
composer built for texting: plain text plus up to two images, merge tags for
personalization, and a live counter that shows exactly how many segments - and
credits - each message costs as you type.
* **Separate SMS consent** - every subscriber has a phone field and an SMS
consent status independent of their email status. Set consent when importing
via CSV (with column mapping), through the API, or manually in the app.
Consent is never inferred from a phone number alone.
* **Compliance built in** - STOP and HELP keywords are handled automatically,
and quiet hours keep sends inside 11am-8pm ET (from 2pm on Sundays). Messages
triggered outside the window are delivered when it reopens, never dropped.
* **Click tracking** - links are shortened automatically and clicks feed into
your stats and revenue attribution, just like email.
* **Prepaid credits** - buy credits up front, get a low-balance email alert
before you run out, and top up in a click.
* **Replies land in your inbox** - when a subscriber texts back, the reply is
forwarded to your account email.
[View documentation](/concepts/sms)
### More Reliable Domain Verification
Domain verification is now far more resilient, so verified domains stop
flipping to "failed" because of temporary DNS hiccups.
* **No more false failures.** A verified domain is only marked failed after
several consecutive checks confirm the records are really gone. A DNS lookup
that times out or errors no longer counts against your domain, and we recheck
within minutes instead of hours whenever a check is inconclusive.
* **Delegated DNS zones now verify correctly.** If your sending subdomain is
hosted as its own DNS zone (for example, a separate zone for
`mail.yourdomain.com`), our checks now query the right nameservers. These
setups previously could fail verification even with correct records.
* **Sends are held, not lost.** If a domain is temporarily unverified, queued
emails are delayed and retried automatically for about 20 minutes instead of
failing immediately - enough to ride out a brief verification blip.
* **Flexible SPF records.** Your SPF record can include other providers
alongside `include:amazonses.com` (and use `-all`); verification no longer
requires an exact match.
* **Tracking domains work behind Cloudflare.** Custom tracking domains proxied
through Cloudflare (orange cloud) now verify correctly.
* **Internal verification alerts.** If a verified domain loses verification,
Sequenzy logs the transition internally with the record that needs
attention. The dashboard banner continues to show affected domains.
### API Key Permission Scopes
API keys can now be restricted to a specific set of permissions instead of
always having full workspace access. Every API request checks the key's
permissions and returns a clear 403 error naming any missing scope.
When creating a key - in the dashboard, through the API, the CLI
(`sequenzy api-keys create --preset ...`), or the `create_api_key` MCP tool -
you can pick a **preset** (Full access, Read-only, Safer agent access,
AI drafting, Data ingest with or without automations, Transactional sender,
or Marketing sender) or select **custom permissions** scope by scope.
A few things worth knowing:
* **Existing keys are unchanged** and keep full access.
* **AI agent logins now default to Safer agent access.** When you approve a
CLI or MCP connection, the consent screen lets you choose the key's
permissions, and the default can inspect data and draft content but cannot
send live emails, delete records, manage the team, or create API keys.
Pick Full access on the consent screen if your agent needs to send.
* **Live delivery is opt-in.** Sending requires explicit permissions such as
`campaigns:send`, `transactional:send`, `sequences:activate`, or
`automations:trigger`. Keys without `automations:trigger` can still sync
subscribers, events, and orders, but those writes will not start or continue
automations, and creating subscribers that would queue a double opt-in
email is rejected.
* **SMTP checks permissions too.** Authenticating to the Sequenzy SMTP relay
requires a company key with the `transactional:send` permission.
[View documentation](/authentication)
### Code In The Email Editor
You can now show code in your emails, both inline snippets and full multiline
blocks, so things like coupon codes, variable names, API keys, and commands
stand out clearly.
For **inline code**, select any text and use the new **Code** button in the
formatting toolbar, or wrap a word in single backticks (`` `SAVE10` ``).
For longer snippets, add a dedicated **Code block** from the `/` insert menu (or
the add-block button). It is a standalone block you can drag and reorder like
any other, with a multiline editor that preserves your indentation and line
breaks exactly.
Code blocks support **color themes** - Light, Dark, Midnight, Terminal, and
Solarized. Open the block's settings panel to switch themes. Themes use plain
inline colors (no syntax highlighting), so they render reliably across email
clients, and the block looks identical in the editor and the delivered email.
[View documentation](/guides/email-editor)
### Resend Campaigns To Non-Openers
After a campaign finishes sending, you can now resend it to everyone in the
same audience who didn't open it - a simple way to recover opens without
rebuilding the audience.
On a sent campaign, use the new **Send to non-openers** button. It shows an
estimate of how many people haven't opened yet, then creates a draft that
reuses the original audience with a "didn't open this campaign" rule added. The
draft isn't sent automatically, so you can review it and tweak the subject line
before scheduling or sending.
* Available 6 hours after the campaign finishes sending, so opens have time to
register.
* Works for any audience - all subscribers, lists, saved segments, or filtered
audiences. Manual exclusions are preserved.
* Also available via the API, CLI (`sequenzy campaigns resend-to-non-openers`),
and MCP (`resend_campaign_to_non_openers`).
[View documentation](/api-reference/campaigns/resend-to-non-openers)
### Conditional Blocks Can Branch On Request And Event Variables
Conditional (if/else) blocks in the email editor can now branch on a custom
variable passed in your transactional send `variables` or an automation `event`
payload, not just stored subscriber fields.
Add a condition, choose the new **Custom variable** field, and type any
`{{merge tag}}` path - including nested paths like `order.total` or
`event.plan`. Compare with equals, contains, numeric operators (`>=`, `<=`,
`>`, `<`), or use "is empty" / "is not empty" to check whether the variable was
provided.
For example, sending with `"variables": { "plan": "pro" }` renders the IF branch
of a `plan equals pro` condition and the OTHERWISE branch for everyone else - so
a single template can adapt its content per request.
[View documentation](/concepts/transactional-emails)
### Raw Campaign And Sequence Event Logs
You can now inspect the raw event stream behind campaign and sequence metrics. Use it when you need to audit exactly which subscribers were delivered, opened, clicked, bounced, complained, unsubscribed, or hit delivery delays.
Start with deliveries to verify recipients:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/campaigns/camp_abc123/events?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
```
Expand the stream when you need engagement or deliverability details:
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/metrics/sequences/seq_abc123/events?eventTypes=delivery,open,click,bounce&period=30d" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The same workflow is available from the CLI:
```bash theme={null}
sequenzy events --campaign camp_abc123 --limit 100
sequenzy events --sequence seq_abc123 --event-types delivery,open,click --period 30d --json
```
Agents can use the new MCP tools `list_campaign_events` and `list_sequence_events`. By default, engagement events exclude detected scanner and preview activity; pass `includeMachineEngagement=true` or `--include-bots` when you need the unfiltered raw stream.
[View campaign events documentation](/api-reference/analytics/campaign-events)
[View sequence events documentation](/api-reference/analytics/sequence-events)
### Wait Until A Date In Sequences
Sequence Delay steps can now wait until a date pulled from the triggering event, not just a fixed number of days or hours.
Turn on the **Wait until a date** switch in a Delay step, then point it at a property in the event payload (for example `renews_at` or `booking.startsAt`). The subscriber resumes exactly when that date arrives.
* The date can be an ISO date string or a Unix timestamp
* Add an optional offset to resume before or after the date, like **1 day before** a renewal or **2 hours after** a booking
* Choose what happens if the date is missing or invalid: continue to the next step, or exit the sequence
* If the resolved date is already in the past, the step continues immediately
This makes it easy to build renewal reminders, appointment follow-ups, and trial-expiry nudges that line up with each subscriber's own dates.
[View documentation](/concepts/sequences)
### Agent Auth Discovery And Current Limits
Sequenzy now publishes `auth.md` and OAuth discovery metadata so agents can find the supported user-approved registration flow for MCP/API access.
This does not mean Sequenzy is fully agent-friendly yet. MCP, CLI, and auth discovery cover many common workflows, but some setup, billing, destructive account actions, advanced configuration, and final send decisions may still require dashboard use or explicit human review.
[View MCP documentation](/concepts/mcp)
### Inbound Reply Webhook Events
Outbound webhooks can now subscribe to `email.replied`, so your app can react as soon as a subscriber replies to a tracked email.
The event fires after Sequenzy stores the inbound reply and inbox conversation message. Its payload includes the reply ID, conversation ID, original email send context, subscriber or external ID when available, sender details, body text, HTML body, stripped reply text, attachment metadata, and received timestamp. Attachment bodies are not sent in webhook payloads.
You can select `email.replied` from the dashboard webhook event picker, pass it through the public webhooks API, use it with the CLI, or configure it through MCP tools. It is opt-in and not part of the default webhook event set.
[View outbound webhook documentation](/integrations/outbound-webhooks)
[View reply tracking documentation](/concepts/reply-tracking)
### 20% Off Your First Year For New Accounts
New workspaces now get 20% off the first year of any annual plan when they upgrade within their first 5 days. Pick Yearly at checkout during the offer window and the discount is applied automatically - there is no code to enter, and the plan renews at the standard annual price after year one.
This replaces the previous early-bird "+5% extra emails" bonus. Accounts that already claimed the email bonus keep it for the full 6 months; the new discount applies to upgrades going forward.
We also added a one-time upgrade nudge that appears right after you activate your first sequence or send your first campaign. While the offer is live it highlights the 20% discount with a countdown, so it is easy to lock in annual pricing at the moment you are getting value. The in-app offer banner and pricing screen show the discounted first-year price up front, and the Upgrade button in the sidebar lights up with a live countdown while the offer is running so you never lose track of it.
### Webflow Marketplace Install Onboarding
The Webflow Marketplace install page now gives signed-out visitors a guided
Sequenzy onboarding step before OAuth. Existing users can sign in and continue,
new users can create an account, and both paths keep the redirect back to the
Webflow install flow so authorization can continue without losing context.
The sign-in and sign-up screens also show a Webflow-specific install notice
when they were opened from the marketplace flow, making it clear that the next
step is authorizing Webflow form capture.
[View documentation](/integrations/webflow)
### Click Metrics To Chart Them
The trends chart on the Metrics dashboard is now interactive. Click any Overview card - Emails sent, Deliverability, Open rate, Click rate, Unsub rate, Reply rate, or Revenue - to add or remove its line from the chart, so you can compare several metrics over time at once. Emails sent and Open rate are shown by default.
Each card and its line share a color, the legend shows the total for every selected metric, and hovering any point lists the value and rate for each line on that day. Revenue is plotted on its own currency axis so it stays readable next to email volumes.
### Backfill Existing Shopify App Merchants
Connecting a Shopify app now has a way to bring in the merchants you already have. Shopify never replays past installs, so the live webhooks only cover changes going forward - the new bulk identify endpoint lets your backend push every current merchant in one call.
Each merchant is created or updated as a subscriber, gets its Shopify app attributes plus `mrr` and `ltv`, and is marked installed. You can carry the plan price and let Sequenzy derive MRR (yearly prices are normalized to monthly), or pass `mrr` and `ltv` directly for exact figures. Send up to 1000 merchants per request. The backfill never triggers automations, so seeding existing merchants will not email them.
[View documentation](/integrations/shopify-app)
### Shared Domain Warning Before Large Sends
Sending a big campaign from a shared sending domain (one of the `sequenzymail.com` addresses) now prompts a heads-up before it goes out. When a campaign to more than 500 subscribers is still on a shared domain, pressing send opens a short reminder that shared domains often land in spam, with a 10-second pause before "Send anyway" unlocks and a one-click path to connect your own domain for better inbox placement.
It only appears when it matters - smaller sends and campaigns already on a verified custom domain are unaffected - and you can tick **Don't show this again** to dismiss it for good.
[Connect your domain](/guides/domain-verification)
### SMTP Settings In The Dashboard
Set up SMTP sending without leaving your workspace. **Settings → SMTP** now shows your full connection details - host, port, username, and encryption - each with one-click copy, plus a ready-to-paste Nodemailer example.
You can generate the API key that doubles as your SMTP password right from the same screen. Click **Generate API key** and it is created, copied to your clipboard, and shown once so you can store it safely - no more hopping to the API Keys page mid-setup. Generating a key requires an admin role; everyone else sees the connection settings and a pointer to request one.
[Send with SMTP](/send-email/smtp)
### Unsubscribe Page Now Follows Your Branding
The unsubscribe and email preferences page no longer shows the Sequenzy website header, footer, or live chat widget. Your subscribers now see a clean, standalone page with your company logo, name, and brand color applied to the controls - the save button and subscription toggles match your branding instead of Sequenzy's.
For free-tier workspaces, the page shows the same "Powered by Sequenzy" pill that appears in emails. Paid plans stay fully white-label.
### Saved Signup Forms
Signup forms are now saved in your dashboard instead of living only in copied embed code. Create as many forms as you need - one per page, campaign, or audience - and manage them from **Audience → Forms**.
**What's included:**
* Each form gets its own name and submission endpoint, with submission counts shown next to every saved form
* Audience targeting (lists, tags) and success behavior (message or redirect) are stored server-side, so editing them in the dashboard updates forms already deployed on your website - no re-embedding needed
* Edit, embed, or delete any saved form from the Forms screen
* Embed forms with a one-line JavaScript snippet or full HTML
* Plain HTML embeds set to show a success message now get a hosted confirmation page instead of raw JSON
* Free workspaces show a small "Powered by Sequenzy" pill under embedded forms and on the hosted confirmation page. Upgrading removes it - regenerate the embed code after upgrading to drop it from forms already on your site
* Existing deployed embeds keep working unchanged on the previous company-level endpoint
[View documentation](/widgets/signup-form)
### Meta Custom Audience Sync
Push any segment to a Meta custom audience and keep it in sync automatically. Run Facebook and Instagram ads that target - or exclude - exactly the same people as your email campaigns, without exporting CSVs.
**What's included:**
* Connect your Meta account in Settings → Integrations under the new Ads & Audiences section
* Map any segment to a custom audience in one of your Meta ad accounts, with hourly, daily, or weekly refresh
* One-click ready-made segments built for paid social: **Zero LTV**, **No purchase in 1 year**, **Recent buyers**, **High spenders**, **Non-buyers**, **Engaged**, and more - picked to match your store or SaaS setup
* Manual "Sync now", pause/resume, and per-sync status with last-synced counts
* Emails are SHA-256 hashed before reaching Meta; only active subscribers are uploaded
* Full agent-surface support: manage syncs via the [API](/api-reference/audience-syncs/list), the CLI (`sequenzy audience-syncs`), and MCP tools (`create_audience_sync` and friends)
Audiences are add-only (Meta doesn't support removals through this integration), and Meta requires 100+ matched people before an audience is usable for delivery.
[View documentation](/integrations/meta-ads)
### Campaign Revenue Attribution for E-Commerce Orders
See how much money a send made. Commerce orders (Shopify, WooCommerce, and the Commerce API) now feed the built-in revenue goal, and attributed conversions and revenue show up everywhere campaign and sequence stats do.
**What's included:**
* Every `ecommerce.order_placed` event is attributed to the most recent email open or click within 24 hours (last-touch), using the order total as revenue. Repeat purchases count - unlike SaaS renewals, every placed order is a deliberate buy
* Campaign and sequence stats now include `conversions` and `revenueCents` in the [metrics API](/api-reference/analytics/campaign-metrics), the CLI (`sequenzy stats --campaign`), and the `get_campaign_stats` / `get_sequence_stats` MCP tools
* The dashboard's existing revenue attribution views (campaign list and detail) pick up commerce revenue automatically
* No setup needed - this extends the default revenue goal that already tracks `saas.purchase`. [View documentation](/concepts/goals)
### Segment by Purchased Product
Build segments and target campaigns by the e-commerce products a subscriber actually bought.
**What's included:**
* New "Purchased Product" segment filter with bought / didn't buy / orders at least / orders less than operators
* Works across all commerce providers - Shopify, WooCommerce, and Commerce API orders - by matching order line items, including all your historical orders
* Available in the dashboard segment editor (with a product picker), the segments API, the CLI (`sequenzy segments create --commerce-product shopify:42`), and the `create_segment` / `update_segment` MCP tools. Filter values are provider-scoped (`provider:productId`) so the same product ID on two providers never collides
* Composable with every other filter, including nested AND/OR groups
### Landing Page Actions For API, CLI, And MCP
Landing pages can now be managed from Sequenzy's public API, CLI, and MCP tools, so agents and scripts can handle the same page lifecycle that exists in the dashboard.
**What's included:**
* Create, list, inspect, update, publish, unpublish, and delete landing pages through `/api/v1/landing-pages`
* Connect, update, and verify landing page custom domains from API, CLI, and MCP
* New CLI commands under `sequenzy landing-pages`
* New MCP tools such as `create_landing_page`, `publish_landing_page`, and `connect_landing_page_domain`
* Dashboard **AI Agent Setup** copy now points users toward landing page workflows
[View landing pages documentation](/concepts/landing-pages)
[View MCP documentation](/concepts/mcp)
### Fixes for Shopify Product Currency and Description Formatting
Synced Shopify products now always use your store's own currency. Previously, stores with multiple market currencies enabled could see prices labeled with the wrong currency (often AED) in product pickers and product blocks.
Product prices now match your storefront's default variant instead of the cheapest variant, so the price shown in emails is the same one customers see on the product page.
Product descriptions imported from Shopify and WooCommerce are now converted to clean plain text. Previously, descriptions could show raw HTML tags (like `
` and ``) in the email editor and in sent emails. Existing product blocks with HTML in their descriptions are cleaned up automatically when rendered - no action needed.
Product blocks also gained a description length control: choose to show the full description or just the first 1-5 sentences. The full text stays saved on the block, so you can switch back anytime. The description field in block settings is now a proper multi-line editor as well.
Prices and currencies refresh on the next product sync. If a product block already shows the wrong currency, re-select the product after syncing to update it.
### Double Opt-In Now Covers Events, Webhooks, and Tags
Double opt-in previously applied only to contacts created through signup forms and the subscribers API. Contacts created by events - external app triggers like Tally or Cal.com, the events API, or tag operations - became active immediately and sequences sent right away.
With this change, every path that creates a brand-new contact respects the workspace double opt-in setting:
* New contacts created by events or tags are stored as pending and receive your confirmation email automatically
* Sequences triggered by the event or tag enroll the contact but wait at the trigger step - the first email only sends after the contact confirms
* Confirming resumes all parked sequences at once; enrollments that are never confirmed within the confirmation window are cancelled automatically
* Events and tags are still recorded while confirmation is pending, so segments and analytics stay accurate
* Contacts who unsubscribed earlier are never re-enrolled, and existing active contacts are unaffected
No setup changes are needed - if double opt-in is enabled, it now simply applies everywhere. [View documentation](/concepts/double-opt-in)
### Pick Products Directly in Trigger Filters
When a sequence trigger fires on a product-aware event - order placed, checkout started, replenishment due, back in stock, or a Stripe purchase - you can now pick one or more products from your catalog instead of typing raw IDs.
**What's included:**
* An "Only for products" picker on the trigger, shown for any event that carries a product. It works across all providers: Shopify, WooCommerce, Stripe, and products pushed via the Commerce API
* Select multiple products to start the sequence when the event includes any of them
* Searchable product list with images, provider, and product ID - and an escape hatch to enter a custom ID for products not yet synced
* Property filter rows on product ID fields (like `lineItems[].providerProductId`) now use the same picker for their value instead of a free-text input
* Order and cart events now suggest `lineItems[]` fields (product ID, SKU, title, variant, quantity, price) in the property path autocomplete
* New `one_of` property filter operator that matches when a property equals any entry of a list (up to 50 values) - available in the dashboard, [API](/api-reference/sequences/create), CLI, and MCP
* Picker selections are scoped to the product's platform - one platform per filter, with platform-qualified custom IDs - so a product ID from one store can never accidentally match an identically numbered product from another
* Shopify checkout and refund events and WooCommerce cart/checkout events now include the same normalized `lineItems` as order events, so product filters work consistently across providers (refund events list the refunded products)
The picker writes a standard property filter, so it stays fully compatible with filters created manually, via the API, or through MCP.
### Redesigned Settings Navigation
Company settings now use a grouped sidebar instead of a long row of tabs. All 16 sections are organized into five groups - General, Email & Sending, Data & Automation, Integrations & API, and Workspace - so every destination is visible at a glance and one click away.
Existing links to specific settings sections keep working, and the mobile section picker is grouped the same way.
### Agent-Surface Parity: Run Campaigns, Audience, Team, Inbox, and Webhooks From CLI, MCP, and API
The dashboard is no longer the only place to operate Sequenzy. You can now cancel or pause a sending campaign, manage your audience, invite teammates, triage replies, and manage outbound webhooks from the CLI, MCP tools, and the public API alike.
**What's included:**
* Cancel, pause, or resume a sending campaign - resume can spread the remaining delivery over up to 72 hours - and duplicate or delete campaigns
* Create campaign A/B tests with test percentage, duration, and winner criteria, then add or remove variants
* Manage lists, segments, and tags fully: update and delete them, and remove subscribers from a list in bulk
* Manually enroll up to 500 subscribers into a sequence, starting at any step
* List your team, invite teammates as admin or viewer, and cancel pending invitations
* List, read, and reply to inbox conversations, add internal notes, and open, close, or mark them read
* Create, update, test, and delete outbound webhooks, inspect deliveries, and replay them - the signing secret is shown once at creation. `DELETE /v1/webhooks/:id` now permanently deletes the endpoint and its delivery history (it previously only disabled it); use `PATCH` with `status: "disabled"` to keep an endpoint without deleting it
Destructive CLI commands ask for confirmation and accept `--yes` for scripts and agents.
[View CLI documentation](/concepts/cli)
[View MCP documentation](/concepts/mcp)
### Fixed: Segment Filters on Boolean Attributes
Segment and contact filters on boolean attributes (for example `isActive equals false`) now match subscribers correctly. Previously these filters always returned zero results.
Boolean values are now matched strictly: filtering on `true` or `false` matches boolean attribute values only, and filtering on `0` or `1` matches numeric attribute values only.
### Commerce API: Connect Any E-commerce Platform
You can now connect any e-commerce platform to Sequenzy - custom checkouts, CheckoutChamp, Sticky.io, headless storefronts, or your own backend - and get the same experience as the native Shopify and WooCommerce integrations.
**What's included:**
* Push your product catalog with `POST /api/v1/products` - products power email product blocks, replenishment reminders, and back-in-stock alerts
* Push orders with `POST /api/v1/orders` - one call upserts the customer, triggers `ecommerce.order_placed` (or cancelled/fulfilled/refunded), updates revenue attributes (`ltv`, `totalSpent`, `ordersCount`, `aov`), and schedules replenishment
* Track started checkouts with `POST /api/v1/checkouts` to power abandoned checkout automations
* Register back-in-stock requests with `POST /api/v1/back-in-stock` - subscribers are notified automatically when a product upsert marks the item back in stock
* Safe retries everywhere: pushing the same order twice never double counts revenue
* Pass authoritative `customerTotals` from your platform when backfilling order history
* Manage the catalog without code: `sequenzy products upsert` / `sequenzy products delete` in the CLI (with one-shot deliverable upload via `--file`), and `upsert_products` / `delete_product` MCP tools (`attach_product_file` also accepts a local `filePath` when the MCP server runs locally)
[View documentation](/concepts/custom-commerce)
### Digital Product Delivery for Stripe Products
You can now sell digital products through any Stripe checkout and let Sequenzy deliver the file automatically after purchase.
**What's included:**
* Your Stripe product catalog now syncs into Settings → Products (alongside Shopify and WooCommerce products)
* Attach a deliverable file to any Stripe product - upload it to Sequenzy (PDF, ePub, ZIP, images, audio, video - up to 100MB) or link to an externally hosted file
* Purchases automatically enrich the `saas.purchase` event with `download.url` and `download.name`, so sequence emails can link straight to the file
* A new **Only for product** picker on purchase triggers starts a sequence only when a specific Stripe product is bought
* Product-scoped triggers are also available programmatically: pass `propertyFilters` to `POST /api/v1/sequences`, the `create_sequence` MCP tool, or `sequenzy sequences create --property-filters-json`
* The email editor suggests the download variables when building purchase sequences
* Works with Stripe payment links and Stripe Checkout - no checkout changes required, and buyers become subscribers automatically
* Full API support: list products, attach/remove delivery files, presigned file uploads, and catalog sync via `/api/v1/products`
* CLI support: `sequenzy products list`, `sequenzy products attach-file prod_abc --file ./guide.pdf`, `sequenzy products sync`
* MCP support: `list_products`, `attach_product_file`, `remove_product_file`, and `sync_products` tools
[View documentation](/concepts/digital-products)
### Hosted Landing Pages
You can now create hosted landing pages in Sequenzy and route signups directly into your subscriber lists, tags, and automations.
**What's included:**
* Build pages from templates for waitlists, lead magnets, launches, webinars, newsletters, demo requests, seasonal offers, agency leads, and feature announcements
* Edit sections, blocks, images, copy, desktop/mobile previews, SEO metadata, and page-level style settings in one workspace
* Add a native signup form with first name, last name, custom placeholders, success messaging, redirect URLs, list routing, and signup tags
* Publish draft pages to Sequenzy-hosted URLs, then copy or preview the public link from the page list or editor
* Connect a custom domain with CNAME verification and optional automatic Cloudflare DNS setup when available
* Track views and leads for each page from the landing-pages list
[View landing pages documentation](/concepts/landing-pages)
### WooCommerce Plugin On WordPress.org
Sequenzy for WooCommerce is now available through the WordPress.org Plugin Directory.
**What's included:**
* Install the WooCommerce plugin from its public WordPress.org listing
* Keep the guided Sequenzy pairing flow from `WooCommerce -> Sequenzy`
* Sync WooCommerce products, customers, guest buyers, orders, carts, and checkout activity
* Monitor plugin connection health and run manual store syncs from `Settings -> Integrations`
* Use the dashboard integration setup modal to open the WordPress.org install page instead of downloading a ZIP
[View WooCommerce plugin documentation](/concepts/woocommerce-plugin)
### Live Sequence Test Runs
You can now run a full sequence against one real subscriber before turning it loose.
**What's included:**
* Start a **Run sequence test** directly from the sequence builder
* Select one existing active subscriber so merge tags, branches, tags, lists, attributes, discounts, and webhooks use real subscriber state
* Compress waits with speed options like 60x, 120x, 240x, and 480x, so long onboarding or recovery flows can be checked quickly
* Send sequence emails through the test-email path with a `[TEST]` subject prefix while still creating normal send records for tracking and history
* Review recent test runs in the builder, including step status, accelerated wait timing, sent-email references, and failures
* Prevent accidental cross-sequence loops when test actions add tags or lists to the subscriber
Non-email actions run for real on the selected subscriber, so use a safe internal or test contact when checking destructive or webhook-heavy flows.
[View sequence documentation](/concepts/sequences)
### Custom Reply-To Domains For Reply Tracking
Reply tracking can now use your verified sending domain instead of the shared Sequenzy inbound domain.
**What's included:**
* Choose **Your domain** or **Sequenzy** for trackable reply-to addresses from `Settings -> Email Tracking`
* Use branded reply addresses like `reply+{id}@inbound.yourdomain.com` after the inbound MX record and SES receipt rule are active
* Fall back automatically to `reply+{id}@inbound.sequenzy.com` when a custom inbound route is not ready
* Show the required inbound MX record in domain setup, including automatic DNS creation where supported
* Register and clean up SES receipt rules for customer inbound domains as sending domains are verified or removed
* Validate inbound reply domains before threading replies into conversations
[View reply tracking documentation](/concepts/reply-tracking)
[View domain verification guide](/guides/domain-verification)
### Sequenzy Signup For Framer
The Sequenzy Signup form is now available as a Framer component, making it
easier to collect subscribers from Framer sites without copying custom embed
code.
**What's included:**
* A ready-to-use Framer component for email signup forms
* Form ID, list routing, tag IDs, copy, layout, colors, radius, font size, and
redirect settings in Framer's right panel
* Submissions sent directly to Sequenzy's public form endpoint, with no API key
required in Framer
* Support for both component-based setup and Framer's native Form webhook flow
[Open in Framer Marketplace](https://www.framer.com/marketplace/components/sequenzy/)
[View Framer documentation](/integrations/framer)
### WordPress Plugin On WordPress.org
Sequenzy Email Marketing is now available through the WordPress.org Plugin Directory.
**What's included:**
* Install the WordPress plugin from its public WordPress.org listing
* Keep the guided Sequenzy pairing flow from `Settings -> Sequenzy`
* Sync WordPress users, profile updates, and optional comment opt-ins
* Monitor plugin connection health from `Settings -> Integrations`
[View WordPress plugin documentation](/concepts/wordpress-plugin)
### Circular Countdown Timers And Expired States
The countdown block gains two round styles and control over what shows after the deadline passes.
**What's included:**
* Two new **Circle** styles: an outlined dial and a solid accent disc, each with a sweeping progress ring that empties as the days/hours/minutes/seconds tick down
* A **When expired** setting that lets you either freeze the timer on 00:00:00 or replace the digits with a custom message such as "Offer expired"
* A live preview of the expired state right in the block settings, so you can see exactly what recipients get once the timer runs out before you send
[View documentation](/concepts/countdown-timers)
### Countdown Timers
Add a live countdown timer to any campaign or automation email to create real urgency around a deadline.
**What's included:**
* A native **Countdown** block in the email editor, available from the block menu
* Four styles (Boxed, Minimal, Bold, Outlined) that automatically use your brand primary color
* Renders as a server-generated animated GIF that recalculates the time remaining on every open, so it works in Gmail, Apple Mail, and Outlook with no JavaScript or third-party service
* **Localized labels**: the DAYS / HOURS / MINS / SECS labels are translated to your company's email language automatically (Ukrainian, German, French, and 15 more), baked right into the image
* Pick a deadline with a calendar and quick presets (Tonight, Next Monday, End of month...), choose whether to show unit labels, and optionally link the timer to your offer page
The timer counts down from the moment each recipient opens the email. Because some clients (notably Gmail's image proxy) cache images, a re-open within a short window can briefly show a slightly stale time, but the first open is always accurate.
[View documentation](/concepts/countdown-timers)
### Dynamic Image Sources In The Email Editor
The image block's settings panel now makes it easier to point an image at a dynamic value.
**What's included:**
* A redesigned, tidier Image Settings panel with clearer source, alt text, and link sections
* One-click "dynamic source" suggestions that prioritize subscriber attributes and fields containing an image or URL
* Inside event-triggered sequences, the actual event payload fields are offered as `{{event.*}}` variables (not just a generic placeholder), so you can drop a product image or screenshot URL straight from the trigger event
### Remote And Local MCP Setup In Dashboard
The existing dashboard **MCP** button now shows both hosted remote connector setup and local stdio setup.
**What's included:**
* Copy the hosted remote MCP URL for Claude.ai, Claude mobile, and other Streamable HTTP clients
* Follow the local stdio setup path for Claude Desktop, Claude Code, Codex, Cursor, Windsurf, and VS Code
* Keep personal-key creation and manual local config snippets in the same MCP setup modal
* Open the full MCP docs directly from the sidebar MCP setup flow
[View MCP documentation](/concepts/mcp)
### Remote MCP Connector For Claude
You can now connect Sequenzy to Claude.ai and Claude mobile through a hosted MCP endpoint.
**What's included:**
* Add Sequenzy as a remote custom connector with `https://api.sequenzy.com/v1/mcp`
* Complete the Sequenzy OAuth flow from Claude without local config files
* Use the same MCP tools from browser and mobile clients that cannot launch a local stdio server
* Keep company selection available across API instances through shared Redis state
* Continue using the existing local `npx -y @sequenzy/mcp` setup for desktop and IDE clients
[View MCP documentation](/concepts/mcp)
### Smarter Sequence Template Picker
The sequence template picker now helps you choose what to launch next based on real subscriber data and projected impact, not just template popularity.
**What's included:**
* **Your next step** hero recommends the highest-impact template you do not already have live, with churn and recovery sequences prioritized over low-value nurture flows
* Projected monthly recovery is calculated from eligible subscribers, benchmark conversion, and your synced MRR or AOV (shows `?` when the estimate is not ready yet)
* Eligible counts use active, emailable subscribers only, with tooltips that explain the math and a link to view matching contacts in a new tab
* Templates you already have running move to a dedicated **Already live** section at the bottom with toned-down styling
* Template cards and the preview modal share the same layout: projection, setup time, payment requirements, and email count chips with helpful tooltips
* The preview modal scrolls properly on both sides and drops the old fake requirements checklist and benchmark clutter
[View sequence documentation](/concepts/sequences)
### Audience-Scoped Metrics And Revenue Attribution
The Metrics page now gives you a clearer view of performance by audience, email type, and business impact.
**What's included:**
* Filter dashboard metrics by all subscribers, a specific list, or a saved segment
* Combine audience filters with campaign, transactional, sequence, or all-email views
* See attributed revenue in the overview when your workspace has revenue-driving goals
* Review audience-scoped delivery trends, country breakdowns, reply metrics, and core engagement rates
* Identify list, segment, and email-type filters faster with icons in the dashboard controls
[View metrics documentation](/concepts/metrics)
### Double Opt-In For Subscriber Signups
You can now require new subscribers to confirm their email address before they become active marketing contacts.
**What's included:**
* Enable double opt-in from email tracking settings after setting a default sender profile
* Automatically create and customize the confirmation email, including the required `{{DOUBLE_OPT_IN_URL}}` link
* Keep pending subscribers unsubscribed until they confirm, then add their target lists, apply signup tags, and trigger the matching automations
* Use `optInMode` in the subscriber API to force double opt-in, confirm verified consent immediately, or obey the workspace setting
* Embedded signup forms now follow the workspace double opt-in setting
[View double opt-in documentation](/concepts/double-opt-in)
### Dark Mode For Everyone
Dark mode is now available to all users from the dashboard, without requiring an admin-only access flag.
**What's included:**
* Switch between light, dark, and system theme from the dashboard sidebar
* Use the command palette to select a theme directly
* Theme preferences persist across sessions for every user
### Matching Stop Conditions For Event Sequences
Event-triggered sequences can now stop only when the stop event matches the same object that started the sequence.
**What's included:**
* Stop conditions for event-triggered sequences can match stop-event fields against the original entry event, so a sequence can stop only when the same order, product, variant, or configured field path appears again
* Choose a matching field for stop conditions in the sequence builder when the trigger is event-based
* Matching stop conditions keep working through draft saves and compatible trigger edits
* Use matching stop conditions for flows such as "stop when the same order is refunded" or "stop when the same product is purchased again"
[View sequence documentation](/concepts/sequences)
### Campaign Computed Lists For Personalized Repeat Blocks
Campaigns can now store campaign-scoped data and compute per-recipient lists at send time, so repeat blocks can show subscriber-specific products, events, posts, offers, or other JSON rows.
**What's included:**
* Add `campaignData` JSON to a campaign for reusable item sources
* Define `computedLists` that filter, sort, limit, and optionally backfill rows from campaign data for each subscriber
* Use computed list keys as repeat block sources in the campaign editor
* Preview the computed output for an existing subscriber or inline subscriber data before sending
* Send, test-send, duplicate, and A/B test campaigns with the same computed-list rendering path
* Use the API and CLI to upload campaign data and computed-list definitions
[View campaign update API](/api-reference/campaigns/update)
[View computed-list preview API](/api-reference/campaigns/preview-computed-data)
[View email editor guide](/guides/email-editor)
### Subscriber Activity And Segment Accuracy
Subscriber profiles and segment filters now show more useful context and stay in sync after profile changes.
**What's included:**
* Subscriber activity timelines now group related send, delivery, open, and click events together
* Each sent-email activity item shows why the email was sent: campaign, sequence step, transactional email, or test email source
* Subscriber attribute edits from the dashboard and API now update segment filters more reliably after save
* Nested array custom attributes stay usable in segment filters when the underlying array values change
* Changing a bounced subscriber to a clean new email address reactivates the subscriber and clears soft-bounce state, while changes to known bounced addresses remain blocked
* Email HTML generation is more reliable for block emails and generated unsubscribe footers
[View subscribers documentation](/concepts/subscribers)
[View sequence documentation](/concepts/sequences)
### Signup Form Targeting, Segment Templates, And RTL Editing
Embedded signup forms, predefined segments, and the HTML editor now cover more common audience and localization workflows.
**What's included:**
* Signup form embeds can target selected lists and apply selected tags, with generated HTML/React snippets using the public forms endpoint
* The public signup form endpoint now accepts `lists` and `tags` query parameters while keeping existing hidden-field formats for deployed embeds
* Predefined segment templates now include more SaaS, ecommerce, and engagement audiences, including templates that use OR logic where it better matches the audience
* HTML editor direction handling now supports RTL emails while preserving existing LTR rendering behavior
[View signup form guide](/widgets/signup-form)
[View signup form API](/api-reference/widgets/submit-form)
[View subscribers documentation](/concepts/subscribers)
[View email editor guide](/guides/email-editor)
### Sequence Branching And Enrollment Controls
Sequence automation now has more precise branching and safer controls for when subscribers can enter a running sequence.
**What's included:**
* Add branch paths to sequence updates, so dashboard, API, CLI, and MCP clients can edit the right path in multi-branch automations
* Add sequence conditions for prior email link clicks and subscriber events, with link suggestions pulled from sequence emails where available
* Add an enrollment gate that can pause new sequence entries while allowing existing active and waiting runs to continue
* Record sequence lifecycle activity events for finished, stopped, and failed runs on subscriber profiles
* Send sequence lifecycle events through outbound webhooks when teams subscribe to those event types
[View sequence documentation](/concepts/sequences)
[View sequence update API](/api-reference/sequences/update)
[View outbound webhooks](/integrations/outbound-webhooks)
### External Subscriber IDs Across APIs And Integrations
Subscribers can now be matched by a customer-owned external ID across more APIs and integrations, reducing duplicate contacts when email addresses change.
**What's included:**
* Create, update, find, and delete subscribers by `externalId`
* Add and remove tags, trigger events, and send transactional emails using `externalId` when the subscriber already exists
* Import subscriber external IDs from CSV files and show them in subscriber tables and profiles
* Use external IDs from CLI, MCP, Zapier, and n8n workflows
* Map Supabase webhook columns into subscriber external IDs so future updates merge into the same subscriber even when email changes
[View external subscriber API](/api-reference/subscribers/external)
[View subscribers documentation](/concepts/subscribers)
[View Supabase integration](/integrations/supabase)
### Sequence Enrollment Cancellation From API, CLI, And MCP
You can now stop active or waiting sequence enrollments from automation clients without disabling the whole sequence.
**What's included:**
* Use the public API to cancel enrollments inside a required sequence
* Target one subscriber by `subscriberId`, or target all matching enrollments by entry-event field values
* Run field-value cancellation as a dry run before applying bulk changes
* Use the same workflow from the Sequenzy CLI and MCP `cancel_sequence_enrollments` tool
* Skill guidance and OpenClaw-facing docs now describe when agents should use subscriber-targeted or field-value cancellation
[View API reference](/api-reference/sequences/cancel-enrollments)
[View MCP documentation](/concepts/mcp)
### Outbound Webhooks Available To Workspaces
Outbound webhooks are now available from workspace settings and the public API, so teams can send email lifecycle events to their own systems without a private access flag.
**What's included:**
* Create and manage signed webhook endpoints from the dashboard
* Use the public `/v1/webhooks` API to list, create, update, test, replay, and inspect webhook deliveries
* Receive email lifecycle events for sent, delivered, delayed, bounced, complained, opened, clicked, and unsubscribed messages
* Webhook payloads include the Sequenzy subscriber ID and, when available, the customer-owned subscriber external ID
[View documentation](/integrations/outbound-webhooks)
[View API reference](/api-reference/webhooks/outbound)
### AI Images, HTML Import, And Website-Free Workspaces
Email creation and onboarding now work better when you need assets, existing HTML, or a workspace that does not start from a website URL.
**What's included:**
* Generate AI images from inside the email image dialog, with support for brand context, product/reference uploads, and background worker processing
* Import full HTML emails by uploading an HTML file directly into the editor
* Create workspaces without a website URL, with template filtering and product-info setup adapted for website-free companies
* Show a clearer landing-pages waitlist experience for workspaces waiting on hosted landing page tools
[View email editor guide](/guides/email-editor)
### Repeat Blocks For Dynamic Email Content
The email editor can now repeat a group of blocks for each item in a list, making product grids, event lists, recommendations, and transactional line items easier to build.
**What's included:**
* Add repeat email blocks that render collections from subscriber attributes, event data, transactional variables, and preview data
* Add repeat grid layouts for multi-column repeated content in the editor, preview, test sends, and sent email HTML
* Add repeat-block variable helpers and preview data tools so nested item variables are easier to inspect before sending
* Use repeat blocks in campaigns, transactional emails, sequence emails, and test sends through the same rendering path
[View email editor guide](/guides/email-editor)
[View transactional emails](/concepts/transactional-emails)
### Test Email Sending Safeguards
Test emails now behave more like a deliberate review step, with clearer recipients and stronger checks before anything leaves Sequenzy.
**What's included:**
* Test emails now require explicit recipients, carry source metadata, and are accounted for consistently across campaign, sequence, and transactional flows
* Transactional test sends run through stronger phishing-guard checks before sending
* Test-send activity now carries enough context to distinguish campaign, sequence, and transactional previews
* Domain setup checks are stricter before test emails use a sending domain
[View campaign documentation](/concepts/campaigns)
[View domain setup guide](/guides/domain-verification)
### Custom Tracking Domains And Branded Link Routing
You can now route email tracking through your own verified tracking domain, with safer shared fallbacks that avoid sending recipients through `sequenzy.com` or `api.sequenzy.com`.
**What's included:**
* Custom tracking domains can be connected with a CNAME and verified against Sequenzy's managed tracking target
* Click, open, unsubscribe, and Sequenzy branding links use the customer's verified tracking domain when one is configured
* Emails sent from shared `sequenzymail*` sender domains use matching shared tracking hosts where available
* Emails without a custom tracking domain fall back to `sequenzylink.com` instead of direct `sequenzy.com` tracking links
* Existing sent-email tracking links remain accepted, so older campaigns keep working while new sends use the updated routing
[View documentation](/concepts/tracking)
[View domain setup guide](/guides/domain-verification)
### Managed Replies, Regional Sending, And Pause Controls
Reply routing and deliverability controls are more visible and reliable across campaigns, sequences, and managed sending domains.
**What's included:**
* Accept inbound mail for managed domains so replies can be routed through Sequenzy-owned infrastructure
* Forward replies sent to system domains back to the configured destination inbox
* Automation sends now use tracked reply-to routing where available, so sequence replies are recoverable like campaign replies
* SES sending now routes by the sending domain's configured region, including region-aware system domains
* Admins can manage company sending limits and see pause metadata for campaigns and sequences
* Campaigns and sequences now show clearer pause reasons and pause sources in dashboard lists and detail screens
* Paused send failures are hidden from failed views, and paused sequence sends are treated as queued instead of failed
* Company-specific sender health thresholds can override the default bounce, complaint, and pause policy settings
[View campaign documentation](/concepts/campaigns)
[View sequence documentation](/concepts/sequences)
[View domain setup guide](/guides/domain-verification)
### Transactional Email APIs
Transactional emails now have broader public API and MCP coverage, so teams can manage templates without using only the dashboard.
**What's included:**
* Create transactional emails through the public API and MCP tools
* List, retrieve, and update transactional emails through the public API and MCP tools
* Update transactional names, slugs, sender profiles, reply profiles, templates, and enabled state through the API
* Use the same transactional management workflows from automation clients through MCP
[View transactional email documentation](/concepts/transactional-emails)
[View transactional create API](/api-reference/transactional/create)
[View transactional update API](/api-reference/transactional/update)
### Subscriber Attribute Editing And Segment Updates
Subscriber profile editing is more complete in the dashboard, and saved changes become usable in segment filters without extra manual work.
**What's included:**
* Subscriber attribute changes from dashboard edits, imports, webhooks, and payment syncs now update segment filtering in the background
* Add custom attribute creation and JSON editing tools on subscriber profiles
* Add manual subscriber refresh controls and clearer import progress handling
[View subscriber documentation](/concepts/subscribers)
### Bulk Subscriber Imports And Locked Discounts
Admins can import subscribers in larger batches, and Stripe discount actions can create codes locked to individual subscribers.
**What's included:**
* Allow company admins to run bulk subscriber imports
* Add subscriber-locked Stripe discounts for one-code-per-subscriber discount workflows
[View subscriber documentation](/concepts/subscribers)
[View Stripe integration](/integrations/stripe)
### Longer Campaign Spread Windows
You can now spread campaign sends over a configurable 1-72 hour window, giving larger campaigns and warmup sends more room to pace delivery.
**What's included:**
* Campaign scheduling now supports spread durations up to 72 hours
* Resume controls accept the same 1-72 hour spread range for paused campaigns
* The campaign wizard slider now exposes the expanded spread window
[View documentation](/concepts/campaigns)
### Commerce Product Automations, Back-In-Stock, And Subscriber Revenue Cleanup
Commerce workflows now carry product context end to end, from synced store products to sequence stop conditions and subscriber profiles.
**What's included:**
* Shopify products now sync with variants, stock state, images, URLs, pricing, and replenishment settings
* Shopify reconnects now verify the actual granted OAuth scopes before reusing a saved token, so stores missing product/order permissions are prompted to reconnect
* Product-level replenishment reminders can queue after Shopify and WooCommerce purchases, with product-level or variant-level matching
* Replenishment reminder jobs are skipped if the subscriber repurchases the same product before the reminder is due
* Sequences triggered by `ecommerce.replenishment_due` now default to stopping when the same product is purchased again
* The Shopify Notify Me storefront block captures back-in-stock requests, and product sync triggers `ecommerce.back_in_stock` when matching variants return to stock
* Sequences triggered by `ecommerce.back_in_stock` now default to stopping when the same item is purchased or goes out of stock again
* Back-in-stock and replenishment matching now documents the product fields Sequenzy checks: `provider`, `providerProductId`, `providerVariantId`, `lineItems[].providerProductId`, and `lineItems[].providerVariantId`
* Event-triggered sequences now support `matching_field` enrollment with `enrollmentFieldPath`, so you can block duplicate active runs for the same subscriber and event field value while still allowing separate runs for different products, variants, orders, or other event-scoped objects
* The dashboard suggests matching field paths from recent trigger event payloads, and the same field is available through the public API, CLI, and MCP `create_sequence` / `update_sequence` tools
* Subscriber profiles now show purchased products from Shopify/WooCommerce order line items alongside Stripe purchased products
* Subscriber tables now show `AOV` from `aov` or legacy `averageOrderValue`
* Legacy duplicate commerce attributes are cleaned up in the UI: `totalRevenue` is treated as an `ltv` fallback, and `averageOrderValue` is treated as an `aov` fallback
* Dynamic product previews now handle event-scoped product variables, so product blocks backed by `event.product` can still be previewed with sample products
* Product settings now show pending replenishment recipients in a denser list, and known subscribers open directly from that list
[View documentation](/integrations/shopify)
[View documentation](/concepts/woocommerce-plugin)
[View documentation](/concepts/events)
[View documentation](/concepts/sequences)
[View documentation](/concepts/subscribers)
[View documentation](/guides/email-editor)
### Dynamic Stripe And Shopify Discount Actions In Sequences, API, CLI, And MCP
Sequences can now create subscriber-specific Stripe and Shopify discount codes before sending follow-up emails, and the same explicit-step workflow is available from the dashboard, public API, CLI, and MCP tools.
**What's included:**
* A new Create Discount sequence action that creates dynamic Stripe coupons/promotion codes or Shopify discount codes during automation runs
* Discount context merge tags like `{{discount.code}}`, `{{discount.percentOff}}`, and `{{discount.expiresAt}}` for later emails in the same sequence
* Public sequence creation now accepts explicit `create_discount` steps alongside email steps
* MCP `create_sequence` and CLI `sequenzy sequences create --steps-file` now document and pass through discount action steps
* Sequence activation now blocks discount workflows until the selected provider is connected, so codes cannot fail at send time because the provider is missing
* Discount expiration dates are validated across the builder and worker so past dates are rejected before unusable codes are created
* Expiration can be set as a relative duration (`expiresInHours`) so flows like "valid for 48 hours" restart per enrolled subscriber, alongside the existing absolute `expiresAt` date
[View documentation](/api-reference/sequences/create)
[View documentation](/api-reference/sequences/enable)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Nested Segment Logic, Event Filters, And Segment Filters
Segments can now express nested AND/OR logic, custom subscriber event conditions, and saved-segment composition.
**What's included:**
* Nested filter groups for rules like `last_login within 90 days AND (plan_end is empty OR plan_end is before today)`
* Custom event filters such as `saas.purchase:30d`, `saas.purchase:all`, and `saas.purchase:5:30d`
* Segment filters that reference another saved segment by ID, with cycle and depth guards during evaluation
* Existing flat segment filters remain supported and continue to use the legacy wire shape unless nested logic, event filters, or segment filters are used
* Dashboard, public API, CLI, and MCP surfaces now accept the v2 `root` filter shape
* Subscriber filters now include `is_empty` and `is_not_empty` operators for attributes and other nullable fields
[View documentation](/concepts/subscribers)
[View documentation](/api-reference/segments/create)
[View documentation](/concepts/mcp)
### AI Generation Across API, CLI, And MCP
You can now generate draft email content, sequence email drafts, and subject line variants from prompts through Sequenzy's public API, CLI, and MCP tools.
**What's included:**
* New generation endpoints for draft emails, sequence content, and subject line ideas
* `sequenzy generate email`, `sequenzy generate sequence`, and `sequenzy generate subjects` now return real generated content instead of placeholder messages
* Campaign creation now supports `--prompt`, with generated blocks used when available so the draft remains editor-compatible
* MCP campaign creation can generate prompt-based campaign content before creating the draft campaign
* `/v1` remains accepted as a backwards-compatible alias for `/api/v1`
* Generated sequences are capped at 10 emails across the API, CLI, and MCP surfaces
[View documentation](/api-reference/generate/email)
[View documentation](/api-reference/generate/sequence)
[View documentation](/api-reference/generate/subjects)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Supabase Auth Emails Through Sequenzy SMTP
You can now route Supabase Auth emails through Sequenzy SMTP and provision the matching transactional templates from the Supabase integration flow.
**What's included:**
* One-click creation of the default Supabase auth transactional templates from the integration modal
* SMTP JSON payload support for selecting a Sequenzy transactional email by `slug` or `transactionalId`
* Support for both `variables` and `dataVariables` in SMTP template payloads
* Automatic shared-domain sender provisioning when Supabase defaults need a send-ready sender profile
* Updated setup guidance for mapping Supabase template variables to Sequenzy merge tags
[View documentation](/concepts/transactional-emails)
[View documentation](/integrations/supabase)
### Campaign-Specific Engagement Filters
Segments and recipient filters can now target specific sent campaigns instead of only rolling time windows, so you can build rules like "bounced Campaign A but did not bounce Campaign B" without any workarounds.
**What's included:**
* Engagement filters (`emailSent`, `emailOpened`, `emailClicked`, `emailBounced`, `emailComplained`) now accept a specific sent campaign as the value, in addition to the existing 7/30/90/180 day windows
* The dashboard value picker now groups rolling time windows and individual campaigns into separate sections so each option is obvious
* Filter pills now render campaign-scoped conditions naturally (`bounced in "Welcome email"` instead of a generic chip)
* The AI filter builder understands phrases like "bounced the welcome email but not the onboarding email" and resolves the campaign names against your sent campaigns
* `sequenzy segments create --filter-json` accepts `{"field":"emailBounced","operator":"is","value":"campaign:"}` for all engagement fields
* MCP `create_segment` documents the same `campaign:` value format for engagement filters
[View documentation](/concepts/subscribers)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Target Specific Lists from the Stripe Integration
You can now choose which lists newly-synced Stripe subscribers join. The default is "all lists" for both the webhook and the backfill revenue sync, so the two paths stay consistent.
**What's included:**
* New **Lists for new subscribers** section in the Stripe authorize screen, with an "All lists" (default) or "Specific lists" picker
* Stripe webhook-driven subscriber creation honors the configured lists
* Stripe revenue sync honors the configured lists when creating new subscribers
* Existing subscribers are never re-added to or removed from lists by Stripe sync
**Heads-up for existing integrations:** Previously, the Stripe **revenue backfill** created new subscribers without adding them to any list, while the webhook path added them to every list. Both now use the same "all lists" default. If you re-run the revenue sync and don't want new Stripe customers added to every list, open the Stripe authorize screen and pick "Specific lists" (selecting none is equivalent to the old backfill behavior).
[View documentation](/integrations/stripe)
### OR Segments And Clearer Recipient Counts
Saved segments can now match either all filters or any filter, and the product now makes it clearer when a count includes every matched contact versus only active subscribers who can actually receive a campaign.
**What's included:**
* Saved segments now support both `AND` and `OR` filter logic across the dashboard, API surfaces, CLI, and MCP
* `sequenzy segments create` now supports `--match any` for OR-based segments
* MCP `create_segment` now documents `filterJoinOperator: "or"` for match-any segments
* Segment views now distinguish total matched contacts from active subscribers
* Campaign recipient selection now uses the active subscriber count for saved segments, so the estimate lines up with who will actually be sent
[View documentation](/concepts/subscribers)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Conditional Email Content In The Editor, CLI, And MCP
You can now show or hide individual email blocks based on recipient data, and the same conditional block content now works across the dashboard editor, send pipeline, CLI, and MCP tools.
**What's included:**
* A new **Conditional Display** control in the email editor so blocks can render only when a merge-tag value matches a rule
* Support for conditions like `equals`, `contains`, `exists`, and numeric comparisons
* Preview and send-time evaluation using the recipient's real merge-tag data
* `sequenzy campaigns create|update` and `sequenzy templates create|update` now accept `--blocks-json` and `--blocks-file`
* MCP `create_campaign`, `update_campaign`, `create_template`, and `update_template` now accept `blocks` payloads for editor-compatible content
* Updated API docs for draft campaign updates when using block payloads
[View documentation](/concepts/campaigns)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
[View documentation](/api-reference/campaigns/update)
### Default Reply-To Across Campaign Setup
You can now manage default reply-to behavior more directly when working with campaigns, including from the API, CLI, and MCP tools.
**What's included:**
* New campaigns created through the MCP API now inherit your company's default reply profile automatically
* `PUT /api/v1/campaigns/{campaignId}` now lets you update a draft campaign's reply-to using either a reply profile ID or an existing reply profile email
* `sequenzy campaigns update` now supports `--reply-to` and `--reply-profile` for draft campaigns
* The MCP `update_campaign` tool now exposes the same reply-to fields and validates unsupported combinations before sending the request
* Creating a new reply profile from the dashboard can now make it the company default and immediately apply it in campaign and transactional editors
[View documentation](/api-reference/campaigns/update)
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Dashboard Assistant Follow-Ups
The dashboard assistant is now better at ongoing conversations instead of acting like a one-shot shortcut box.
**What's included:**
* Context-aware starter prompts during onboarding, including questions like `What do I start with?` and `How can I add more about my company?`
* Dashboard prompts focused on review and next actions, including `Tell me what happened last week`
* Scoped performance prompts like `Show my top performing sequence` and best-campaign style questions now return the actual sequence or campaign instead of company-wide totals
* A floating follow-up chat that opens when you continue the conversation, so you can keep asking for explanation or next steps without losing context
* Product-info guidance inside the assistant, so it can point you to the right settings when your company context is thin
[View documentation](/concepts/metrics)
[View documentation](/concepts/sequences)
### CLI And MCP Errors Now Explain The Fix
CLI and MCP failures now return more useful recovery guidance instead of short raw error strings.
**What's included:**
* Human-readable failure descriptions for common auth, network, input, permission, and rate-limit issues
* Concrete next-step guidance in the returned error text so agents can retry with the right fix
* Direct `docs.sequenzy.com` links in CLI and MCP error output for faster self-recovery
* MCP tool failures are now explicitly marked as errors with `isError: true`
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### CLI Login Links For Headless Environments
You can now log in to the Sequenzy CLI from headless terminals and agent shells without relying on the CLI to launch a browser window.
**What's included:**
* `sequenzy login` now prints the approval URL directly in the terminal
* The CLI also shows the approval code so you can verify the right session before approving it
* The login command keeps waiting in the terminal until approval is completed and the API key is stored locally
* A smoother authentication flow for remote shells, AI agents, and other environments where browser launching is unreliable
[View documentation](/concepts/cli)
### Ask The Dashboard To Do It
You can now use the expandable AI workspace on the onboarding screen and dashboard landing page to ask for metrics or trigger common setup actions in plain English.
**What's included:**
* Ask questions like `What are my metrics for last 30d?` and get a live summary without leaving the dashboard
* Type requests like `Create a welcome sequence` to open a matching starter flow directly in the sequence builder
* Use natural-language shortcuts for campaign creation and subscriber import from the same box
* Get the same assistant entry point during onboarding, so you can move from first login to first action faster
[View documentation](/concepts/metrics)
[View documentation](/concepts/sequences)
### Expanded Email Provider Coverage
Email provider detection now recognizes a much broader mix of mailbox providers, including regional consumer inboxes and hosted business platforms.
**What's included:**
* More provider buckets for regional mailbox hosts such as UKR.NET, Seznam, GMX, Onet, Interia, Bluewin, Telenet, and others
* More hosted-provider MX detection for providers such as Rackspace, Amazon WorkMail, OVH, and Titan
* Updated provider labels in filters so buckets like `iCloud` and `Custom domain` read more naturally in the UI
* The same background detection and ClickHouse-backed filtering model, now with broader provider coverage
[View documentation](/concepts/subscribers)
### Filter Subscribers By Email Provider
You can now filter subscribers by their email provider, including Google, Microsoft, Yahoo, iCloud, Proton, and other major mailbox hosts.
**What's included:**
* A new `Email Provider` filter in subscriber and segment builders
* Background detection for new subscribers and touched existing subscribers without blocking imports or API writes
* Canonical provider grouping so Gmail and Google Workspace are both treated as `Google`, and Outlook/Hotmail/Microsoft 365 are treated as `Microsoft`
* Provider values synced to ClickHouse for fast segment filtering
[View documentation](/concepts/subscribers)
### Adjustable Email Container Padding
You can now adjust the full email container padding directly from the theme controls, including reducing the side gutter all the way to `0px`.
**What's included:**
* **Horizontal padding** and **Vertical padding** sliders in the **Spacing** section of Email Themes
* Support for both company-wide default themes and per-email theme overrides
* Matching padding in the editor canvas, preview output, and sent email HTML
* An easier way to make emails feel less cramped on mobile without changing the content itself
[View documentation](/concepts/email-themes)
### Event Properties In Sequence Emails
You can now personalize sequence emails with properties from the event that started the automation.
**What's included:**
* Use merge tags like `{{event.city}}` or `{{event.alert.maxSpeed}}` inside sequence emails
* Event properties are stored on the sequence run, so later emails in the same sequence keep using the original event snapshot
* Automation test emails now accept event preview values so you can verify the output before activating the sequence
* Event property variables are shown in the email preview panel once you add them to your template
[View documentation](/concepts/sequences)
[View documentation](/concepts/events)
### Public And Private Subscriber Lists
You can now choose whether a subscriber list is public or private when you create it.
**What's included:**
* Public list names appear in individual controls on the hosted subscriber email preferences/unsubscribe page. Descriptions also appeared at launch; since September 1, 2026, they remain internal workspace metadata.
* Private lists stay internal and are omitted from those individual controls
* New companies still get `Product Subscribers` and `Newsletter Subscribers` by default, and both are created as public lists
* Public-list copy in the create-list dialog now warns you to use subscriber-friendly names
[View documentation](/concepts/subscribers)
### Sequence Steps Can Now Update Subscriber Lists
You can now add sequence steps that place a subscriber into a list or remove them from a list without leaving the automation builder.
**What's included:**
* A new `Update Lists` step in sequences with `Add to List` and `Remove from List` modes
* List-aware sequence actions in the visual builder and worker runtime
* Support for using subscriber lists as sequence routing primitives, not just trigger conditions
* Add-to-list actions now respect the target list membership state instead of duplicating active memberships
[View documentation](/concepts/sequences)
### Advanced Sender Routing
You can now separate the visible `From` address from the routing domain Sequenzy uses behind the scenes directly in sender profile settings.
**What's included:**
* Sender profiles can keep a visible `From` address on one verified domain and route through another verified sending domain in the same workspace
* Dedicated IP selection now follows the routing domain instead of inferring it from the visible sender address
* Campaigns, sequences, transactional emails, test sends, and forwarded replies all use the same authenticated-domain routing behavior
* Review screens show a subtle `via your-domain.com` hint only when visible sender and routing domain differ
[View documentation](/concepts/sender-routing)
### Dedicated IP Routing For Sending Domains
You can now see dedicated IPs in `Settings -> Domain`, track whether each one is still pending or ready, and connect a ready IP to one or multiple sending domains.
**What's included:**
* A dedicated IP management section inside domain settings
* Dedicated IP status visibility so pending purchases can appear before SES setup is finished
* Multi-domain assignment so one dedicated IP can power several sending domains
* Ready-only assignment so pending IPs stay visible but cannot be attached yet
* Domain detail pages now show when a domain is already routed through a dedicated IP
[View documentation](/concepts/dedicated-ips)
### Stripe Product Segments In CLI And MCP
You can now create Stripe-product subscriber segments directly from the Sequenzy CLI, and MCP now documents the exact filter shape for Stripe purchase-based segments.
**What's included:**
* `sequenzy segments list`, `sequenzy segments count`, and `sequenzy segments create`
* Native CLI flags for Stripe product filters like "bought", "didn't buy", and payment thresholds
* Explicit MCP `create_segment` guidance for `stripeProduct` filters
* Updated CLI and MCP docs so the Stripe-product segment workflow is discoverable
[View documentation](/concepts/cli)
[View documentation](/concepts/mcp)
### Stripe Sync Setup Controls
Stripe now gives you one more setup step after authorization so you can decide exactly which Stripe customers should sync into Sequenzy.
**What's included:**
* Choose which Stripe products count for customer sync
* Optionally keep syncing users who have not made any payment yet
* Pick which synced products are allowed to drive webhook-based updates
* Select extra customer or subscription metadata keys to carry into subscriber attributes
* Reopen these settings any time from the gear icon in `Settings -> Integrations`
[Stripe integration documentation](/integrations/stripe)
### WordPress And WooCommerce Integrations
You can now connect WordPress sites and WooCommerce stores to Sequenzy from `Settings -> Integrations`.
**What's included:**
* A new WordPress integration for syncing users, profile updates, and optional comment opt-ins
* A new WooCommerce integration for syncing products, customers, guest buyers, orders, and recovery events
* Install, connect, reconnect, disconnect, and sync controls in `Settings -> Integrations`
* Automatic initial sync after connection for both integrations
Both integrations are currently in beta.
[WordPress integration documentation](/concepts/wordpress-plugin)
[WooCommerce integration documentation](/concepts/woocommerce-plugin)
### Company And Per-Email Theme Controls
You can now customize the visual system behind your emails at both the company and single-email level.
**What's included:**
* Company-wide email themes that set the default look for all emails
* Per-email theme overrides with a reset option to inherit the company theme again
* Built-in starting presets: Default, Soft, Editorial, and Bold
* Controls for typography, spacing, radius, content width, padding, and core email colors
* Send-time rendering that uses the saved theme across campaigns, automations, transactional emails, and test sends
[View documentation](/concepts/email-themes)
### Event And Tag Trigger Visibility
You can now see which contacts received each event or tag in the last 24 hours, and which sequences those triggers feed into.
**What's included:**
* Event settings now show a last-24-hour contact count for each event
* Tag settings now show a last-24-hour contact count for each tag that was added
* Both settings pages now show which active sequences currently listen for that event or tag
* Paused sequences are shown separately so you can spot flows that would trigger if reactivated
* Subscriber activity now shows which sequence an event or added tag triggered, or would have triggered if the sequence is paused
[Events documentation](/concepts/events)
[Tags documentation](/concepts/tags)
### Reusable Email Components
You can now save blocks or full block sections from the email editor as reusable components, then insert them again in other emails without rebuilding them from scratch.
**What's included:**
* Save a selected block as a reusable component from the Components sidebar
* Save a top-level section made of multiple adjacent blocks
* Reuse saved components from the sidebar, the inline add-block menu, or drag and drop
* Generate a reusable component with AI from a prompt and save it straight into your library
* Saved components reuse the same block system as the editor, so they stay editable after insertion
[View documentation](/concepts/email-components)
### Free AI HTML Email Builder
A free tool for generating responsive HTML emails with AI chat, live preview, and code export - no account required.
**What's included:**
* AI chat that drafts subject lines, preview text, and email-safe HTML from a prompt, URL, or screenshot
* Block-based design editor with slash commands (type "/" to insert blocks)
* Live preview tab with desktop and mobile width toggle
* Editable HTML tab for direct code changes
* Drag-and-drop component sidebar with typography, media, buttons, layout, and footer blocks
* Copy and download actions for the final HTML output
[Try it free](https://www.sequenzy.com/tools/ai-html-email-builder)
### Custom HTML Blocks In The Email Editor
You can now insert a dedicated **HTML** block directly inside the visual email editor when you need to drop in custom markup without leaving the block workflow.
**What's included:**
* A new **HTML** block in the component picker and slash-command menu
* Inline code editing directly on the canvas, with a live preview below it
* Matching render behavior in the editor preview and sent emails
* Support for merge tags like `{{FIRST_NAME}}` inside your custom HTML snippets
* Existing full-email HTML import mode remains available for complete HTML documents
[View documentation](/concepts/html-blocks)
### Multi-Language Email Localization
You can now configure extra email locales at the company level, keep localized variants per template, and have Sequenzy pick the right language for each subscriber at send time.
**What's included:**
* Company-level localization settings for supported locales, fallback locale, subscriber attribute mapping, and raw value mappings
* Per-template localized variants with sync status tracking for `synced`, `stale`, `syncing`, and `failed`
* AI-powered sync from your primary language into the extra locales you support
* Send-time locale resolution across campaigns, automations, transactional template sends, and A/B test variants
* Read-only localization visibility in both CLI and MCP for company settings and template variants
[View documentation](/concepts/email-localization)
### Update Tags Action In Sequences
You can now add tag-management steps directly inside sequences from a single **Update Tags** action in the builder.
**What's included:**
* A new **Update Tags** step in the add-step menu
* An in-step selector to choose whether the step should add a tag or remove one
* Full support for editing existing tag steps after they are added
* Runtime execution for both add-tag and remove-tag actions when contacts move through a sequence
[View documentation](/concepts/sequences)
### Installable Sequenzy Skills
You can now install a versioned Sequenzy skill for AI agents from the public `Sequenzy/skills` repo.
**What's included:**
* A reusable `sequenzy-email-marketing` skill you can install with `npx skills add Sequenzy/skills --skill sequenzy-email-marketing`
* Guidance for authentication, stats, subscriber management, and transactional email workflows
* Clear notes on current CLI gaps so agents fall back to the dashboard or API when needed
* A dedicated skills repo that can grow with more Sequenzy-specific agent workflows over time
[View documentation](/concepts/skills)
[Browse the repo](https://github.com/Sequenzy/skills)
### Reliable Natural-Language Date Filters
Subscriber and segment date filters now use validated ISO dates and full natural-language parsing instead of compact shorthand values.
**What's included:**
* Built-in Added filter presets now use plain-English values like `7 days ago` and `30 days ago`
* Fixed dates like `2026-03-01` still work and are validated before use
* Natural-language expressions like `last week`, `today`, and `3 days from now` stay supported
* Unsupported shorthand inputs like `7d` and `24h` are no longer part of the date-filter contract
[View documentation](/concepts/subscribers)
### Natural-Language Date Filters
You can now use rolling phrases like `30 days ago` and fixed dates like `2026-03-01` directly in subscriber and segment filters.
**What's included:**
* Rolling date expressions like `7d`, `30 days ago`, and `3 days from now`
* Fixed date values like `2026-03-01` and `Mar 1 2026`
* Clear rolling-vs-fixed behavior: relative phrases re-evaluate over time, fixed dates stay fixed
* Date-aware custom attribute comparisons that accept the same date formats
[View documentation](/concepts/subscribers)
### Date-Aware Subscriber Attribute Filters
You can now segment subscribers using custom attributes that contain dates or full timestamps, including ISO datetime values like `2026-03-19T10:56:03+00:00`.
**What's included:**
* Date-only custom attributes like `2026-03-20` are normalized for range filters
* Full ISO datetime strings with offsets are supported for segment comparisons
* Exact-match and range filters now use intentional date parsing instead of partial numeric coercion
* Existing string and numeric attribute filters continue to work as before
[View documentation](/concepts/subscribers)
### Segment Triggers In MCP Sequences
You can now create sequences through MCP with the same saved-segment entry trigger available in the app.
**What's included:**
* `create_sequence` now accepts `trigger: "segment_entered"`
* You can pass a `segmentId` from your saved segments
* MCP resolves the segment name automatically so the created sequence is labeled correctly
* This works for both AI-generated sequence creation and explicit-step sequence creation
[View documentation](/concepts/mcp)
### Segment Exit Stops For Sequences
You can now optionally stop a segment-triggered sequence when a contact no longer matches that segment.
**What's included:**
* A new per-sequence setting on the **Segment entered** trigger
* Automatic cancellation of active and waiting steps when the contact leaves the triggering segment
* The same 8-minute fallback segment evaluation used for enrollments, so exits stay in sync with entries
* No change to baseline behavior for current segment members when you first activate the sequence
[View documentation](/concepts/sequences)
### Segment-Entered Sequence Triggers
You can now start a sequence when a contact newly enters one of your saved segments.
**What's included:**
* A new **Segment entered** trigger in the sequence builder
* Automatic evaluation after relevant subscriber data changes, plus a background check every 8 minutes
* Safe baseline behavior so existing segment members are not enrolled when you first activate a sequence
* Automatic re-baselining when you change the segment definition or reactivate the sequence
* Manual **Enroll Existing Subscribers** support when you do want to send the sequence to current segment members
This makes lifecycle automations like VIP onboarding, trial qualification, and at-risk follow-up much easier to build without relying on extra tags.
[View documentation](/concepts/sequences)
### Flexible Campaign Delivery Planning
You can now choose campaign start time and delivery strategy separately. This means you can schedule a campaign for a future date and still spread delivery over 1-24 hours, instead of choosing between those options.
**What's included:**
* Separate controls for when sending starts and how delivery is paced
* Scheduled + spread delivery support in the campaign wizard
* A clearer delivery summary during setup and review
* Existing Send Time Optimization support for both immediate and scheduled starts
* Warmup recommendations that now suggest spread without forcing immediate send
[View documentation](/concepts/campaigns)
### Automatic UTM Tracking for Email Links
You can now define account-level UTM templates and have Sequenzy append them automatically when email HTML is generated.
**What's included:**
* Account-level defaults for `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, and `utm_term`
* Template variables like `{{email.subject}}`, `{{link.text}}`, and `{{company.name}}`
* Per-link manual override from the email editor when a URL should stay untouched
* Automatic exclusions for unsubscribe, preferences, social, and legal links
* Consistent tagging across buttons, rich-text links, image links, CTA blocks, and product links
[View documentation](/concepts/utm-tracking)
### Delivery Delay Tracking
You can now see delivery delay metrics in your dashboard. When email delivery is temporarily delayed, Sequenzy tracks these events and surfaces them in the metrics section.
**What's included:**
* Delivery delay rate shown as a percentage of total emails sent
* Raw count of delayed emails
* Visual progress bar for quick assessment
* Only displayed when delays are detected, keeping your dashboard clean
Delivery delays are informational: delivery will continue to be retried automatically. Monitoring delays helps you identify potential deliverability issues early.
### Shopify Integration
Connect your Shopify store to Sequenzy and automatically sync customers, products, and order events.
**What's included:**
* **One-click OAuth setup**: Connect your Shopify store from Settings → Integrations
* **Automatic customer sync**: Subscribed customers are imported with their order history and spend data
* **Product catalog sync**: Products are synced and available as drag-and-drop email blocks in the editor
* **Real-time webhooks**: Order placed, cancelled, fulfilled, refunded, customer created, and checkout started events are tracked automatically
* **E-commerce tags and sync rules**: Built-in `ecommerce.customer` tag is auto-applied on first purchase, `lead` tag on customer creation, and `refunded` tag on refunds
* **Product email blocks**: New Commerce category in the editor with small, medium, and large product card layouts
**Requirements:** Shopify store with an active plan. Set `SHOPIFY_API_KEY` and `SHOPIFY_API_SECRET` environment variables.
### Enroll Subscribers at a Specific Step
You can now enroll subscribers at any step in a sequence, not just the beginning. This is especially useful when you add new steps to an existing sequence and want finished subscribers to continue from the new step.
**How it works:**
* Hover over any action node in the sequence editor and click the **Enroll** button
* Choose from finished subscribers or search for any subscriber by email
* Subscribers who already passed the selected step are automatically filtered out
* Selected subscribers start processing from that step, skipping all previous steps
**Use case:** You have a 3-step welcome sequence that 50 subscribers completed. You add steps 4 and 5. Instead of restarting everyone from step 1, enroll them at step 4: they'll only receive the new emails.
[View documentation](/concepts/enroll-existing-subscribers)
### Simpler Domain Setup
Domain verification now requires only **3 DNS records** instead of 5. We replaced the previous DKIM approach (3 CNAME records) with a single TXT record, making setup faster and less error-prone.
**What changed:**
* **DKIM**: 1 TXT record instead of 3 CNAME records
* **SPF**: 1 TXT record (unchanged)
* **MX**: 1 MX record (unchanged)
* **Total**: 3 records instead of 5
### Send Emails Without a Custom Domain
You can now start sending emails immediately using the built-in `sequenzymail.com` domain: no DNS setup required. Every account gets a subdomain like `yourcompany.sequenzymail.com` automatically.
**Good for:**
* Testing your email flows before going live
* Quick experiments and prototyping
* Non-essential communication where inbox placement isn't critical
**Keep in mind:** The shared domain's reputation is shared across all users, so emails may land in spam. For best deliverability, set up a custom domain, which is now easier than ever with only 3 DNS records.
[View domain setup guide](/guides/domain-verification)
### Spread Over Time Campaign Delivery
You can now spread campaign sends over a configurable time window (1–24 hours) to protect your sender reputation. Instead of delivering all emails at once, Sequenzy distributes them linearly: the first batch sends immediately, with remaining batches spaced evenly across the window.
**Key features:**
* Choose "Spread over time" as a delivery option in the campaign schedule step
* Configure the delivery window from 1 to 24 hours using a slider
* Automatic warmup recommendation for accounts that have sent fewer than 10,000 emails and are targeting 1,000+ recipients
* Spread and Send Time Optimization are mutually exclusive: enabling one disables the other
**When to use it:**
* Warming up a new sending domain
* Sending to large lists for the first time
* Maintaining consistent deliverability with large campaigns
[View documentation](/concepts/campaigns)
### Email Analytics API
You can now retrieve email engagement metrics via the API. Get totals for sends, deliveries, opens, clicks, and bounces across any time period: perfect for dashboards, reports, and syncing to external tools.
**Endpoints:**
* `GET /api/v1/metrics`: Account-wide metrics
* `GET /api/v1/metrics/campaigns/:campaignId`: Per-campaign metrics
* `GET /api/v1/metrics/sequences/:sequenceId`: Per-sequence metrics with per-step breakdown
* `GET /api/v1/metrics/recipients`: Per-recipient engagement data (opens, clicks, unsubscribes)
**Key features:**
* Aggregated totals: sends, deliveries, unique opens, unique clicks, unsubscribes
* Calculated rates: delivery rate, open rate, click rate, unsubscribe rate
* Sequence step-by-step breakdown to identify drop-off points
* Recipient-level engagement for CRM sync and support lookups
* Filter by sliding window (1h, 24h, 7d, 30d, 90d) or custom date range
[View documentation](/api-reference/analytics/metrics)
### HTML Email Import/Export
The email editor now supports importing and exporting raw HTML templates. Switch to HTML mode to paste existing email templates or export your designs as HTML for use in other tools.
* **Import**: Paste HTML from any email builder and it converts to editable blocks
* **Export**: Download your email as clean HTML
* **HTML block**: A new block type for embedding raw HTML directly in your emails
### Email Management in Sequence Builder
You can now manage your email templates directly from the sequence builder:
* **Filter emails**: Toggle between used and unused emails to keep your workspace clean
* **Delete unused emails**: Remove emails that aren't referenced by any sequence step
* **Usage protection**: Emails in use by sequence nodes or A/B tests cannot be deleted
### Subscriber Filters Persist on Navigation
Subscriber list filters (search, sort, tags, segments, pagination) are now preserved in the URL. When you click into a subscriber's detail page and navigate back, your filters are exactly where you left them.
### Dodo Revenue Tracking
Revenue tracking for Dodo Payments webhooks has been improved with proper event deduplication, accurate MRR/LTV calculation across multiple subscriptions, and correct handling of one-time payments vs. recurring subscriptions. Provider logos now appear in the subscriber activity timeline for better visibility.
### n8n Integration
Sequenzy is now available as a community node in [n8n](https://n8n.io), the open-source workflow automation platform. Connect Sequenzy to hundreds of apps and build complex automation workflows with a visual editor.
Install the `n8n-nodes-sequenzy` community node to get started.
### Soft Bounce Rate Enforcement
Sequenzy now monitors soft bounce rates alongside hard bounces and complaints. If a campaign exceeds soft bounce thresholds, it will be automatically paused to protect your sender reputation.
Soft bounce thresholds are \~2-3x more lenient than hard bounces since they represent temporary delivery failures. You can view your soft bounce status on the Sender Health page with color-coded indicators.
### Bounce Diagnostics
Bounced emails now show detailed explanations to help you understand and resolve delivery issues:
* **Bounce type breakdown**: Permanent, Transient, or Undetermined with specific sub-types (e.g., "mailbox full", "invalid address")
* **Contextual tips**: Actionable guidance based on the specific bounce reason
* **Bounce severity**: Hard vs. soft bounce labels in the sent emails table
### Campaign Approval Flow
Large campaign sends now go through an approval step before sending. When a campaign targets a large number of recipients, it enters a "Waiting for Approval" state and requires admin review before it can proceed. This prevents accidental mass sends and gives you a chance to review before committing.
### Image Block Improvements
The image block in the email editor now supports:
* **Clickable images**: Add a link URL to make any image clickable
* **Alignment**: Position images left, center, or right
* **Percentage-based widths**: Set image width as a percentage (10-100%) in addition to pixel values
All options work consistently across email clients using email-safe table-based layout.
### A/B Test UX Improvements
The A/B test button in the sequence builder now appears on a long hover (1.5s) instead of immediately, preventing accidental clicks. Sequence enrichment flows have also been improved with better prompts and timing.
### Campaign Engagement Chart
Sent campaigns now include an hourly engagement chart showing opens and clicks over time. This helps you see when subscribers are most active and how engagement develops after sending.
The chart displays in your local timezone and handles multi-day campaigns with clear date labels.
### Adaptive Campaign Sending
Campaign sending now uses adaptive chunk sizing that adjusts based on your sending volume and delivery performance. This improves delivery rates for large campaigns by pacing sends more intelligently.
### Metrics Accuracy Improvement
Click events are now correctly counted as opens too: since clicking a link implies the email was opened. This applies to both individual campaign metrics and the aggregate metrics list, giving you more accurate open rate numbers.
### Provider-Specific DNS Records
The domain DNS setup page now formats record names based on your DNS provider. Most providers auto-append the domain suffix, so Sequenzy shows just the subdomain part. Route53 users see the fully qualified name instead. This eliminates a common setup mistake where users would end up with records like `mail.example.com.example.com`.
### Domain Verification Guards
All email sending paths (campaigns, sequences, transactional emails, conversation replies) now verify that your sending domain is verified before queuing emails. Previously, some paths could attempt to send from an unverified domain, which would fail silently.
### Subscriber Search in Test Emails
The test email panel now includes a search field to find subscribers by name or email. You can also send test emails to yourself even if you're not a subscriber: just click "Send to myself".
### Checkout Abandonment Tracking
Sequenzy now automatically detects when someone visits your payment page but doesn't complete the purchase. A `visited-payment-page` tag is applied to subscribers who have an expired checkout session or \$0 in spend, making it easy to target them with recovery sequences.
**How it works:**
* When a checkout session expires (Stripe, Dodo) or a subscriber has zero spend, they automatically receive the `visited-payment-page` tag
* The tag is automatically removed when any purchase event fires
* Use this tag to trigger a checkout abandonment sequence and recover lost revenue
**Setting up a recovery sequence:**
1. Create a new sequence with the trigger "When tag added: `visited-payment-page`"
2. Set the stop condition to "When tag removed" (so the sequence stops if they purchase)
3. Design your recovery emails: remind them what they're missing
This works automatically with Stripe and Dodo integrations. No additional code or configuration needed.
### Dodo Payments Integration Fix
Fixed an issue where Dodo Payments revenue sync could fail in live environments. Webhook setup validation has also been improved: you'll now get a clear error if you accidentally paste a webhook secret instead of an API key.
### Custom Email Fonts
You can now choose from 50+ fonts for your emails. Select a font in the Email Style panel (left sidebar) and it applies to your entire email.
**Available fonts:**
* **System fonts**: Arial, Helvetica, Georgia, Times New Roman, Verdana, Tahoma, Courier New, and more. These are universally supported across all email clients.
* **Web fonts**: Inter, Roboto, Open Sans, Lato, Montserrat, Poppins, Playfair Display, Merriweather, and 40+ more from Google Fonts. These render in modern email clients with graceful fallbacks.
The font picker shows a preview of each font so you can see how it looks before selecting. Web fonts include a note about email client compatibility.
[View documentation](/guides/email-editor#fonts)
### Automatic Stop Condition Sync
When configuring sequence triggers, Sequenzy now automatically sets sensible stop conditions based on your trigger type. This saves time and ensures your sequences stop at the right moment.
**How it works:**
* **Tag triggers**: When you trigger on tag added (e.g., "trial"), the stop condition auto-sets to "when tag removed", so users exit when they're no longer in trial
* **List triggers**: When you trigger on contact added to a list, the stop condition auto-sets to "when removed from list"
* **Inactivity triggers**: When you trigger on inactivity (e.g., no "login" event for 14 days), the stop condition auto-sets to "when event received", so re-engaged users exit immediately
The stop condition syncs automatically as you configure the trigger. You can always override it manually if needed.
### YouTube Video Block
Embed YouTube videos in your emails. The video block displays a thumbnail with a play button that links to the video. Supports width adjustment and left/center/right alignment.
### Dashboard Command Palette
Press Cmd K to open a fast command palette that lets you jump to any dashboard page (including tabbed views) and reopen your three most recent campaigns or templates.
### MCP Integration & CLI
Sequenzy now supports the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), allowing AI assistants like Claude Desktop and Cursor to manage your email marketing directly.
**What you can do with AI:**
* Add and manage subscribers
* Create email sequences from natural language prompts
* Draft campaigns
* Send transactional emails
* View analytics and performance stats
* Generate email content with AI
**Quick setup:**
```bash theme={null}
npx @sequenzy/setup
```
This wizard logs you in, detects your AI clients (Claude Desktop, Cursor, Windsurf), and configures them automatically.
**New CLI tool:**
We've also released a command-line interface for terminal users:
```bash theme={null}
# Login
npx @sequenzy/cli login
# Add a subscriber
npx @sequenzy/cli subscribers add user@example.com --tag vip
# Send an email
npx @sequenzy/cli send user@example.com --template welcome
# View stats
npx @sequenzy/cli stats
```
**Personal API Keys:**
A new type of API key (`seq_user_`) that's tied to your user account rather than shared company-wide. Perfect for CLI/MCP use: each user has their own keys.
[View MCP documentation](/concepts/mcp) | [View CLI documentation](/concepts/cli)
### Inbound Webhooks with Payload Mapping
Connect external services like Cal.com, Typeform, and Calendly directly to Sequenzy without needing Zapier or other automation platforms.
Each webhook gets a **unique secret URL**: no API keys or special headers needed. Just paste the URL into your external service and it works.
**How it works:**
1. Create a sequence with a trigger like "When `call-booked` event received"
2. Copy the unique webhook URL from the trigger settings
3. Paste the URL into your external service (Cal.com, Typeform, etc.)
4. Send a test webhook: Sequenzy captures the raw payload
5. Map fields from the payload to Sequenzy fields (email, first name, custom properties)
6. Save the mapping: future webhooks trigger the event automatically
**Key features:**
* **No API keys needed**: The URL IS the authentication: just paste and go
* **Any payload format**: Sequenzy captures whatever your external service sends
* **Visual field mapper**: Click on values in the captured payload to map them to subscriber fields
* **Live preview**: See extracted values as you map fields
* **Auto-create subscribers**: Subscribers are created automatically if they don't exist
[View documentation](/integrations/custom-webhooks)
### Sent Emails List in Campaign Review
When reviewing a sent campaign, you can now see a complete list of all emails that were sent. View delivery status, open/click tracking, and individual subscriber details directly from the campaign review page.
### Discord & Reddit Social Links
Added Discord and Reddit to the social media block options in the email editor. You can now include links to your Discord server and Reddit community alongside Twitter, LinkedIn, Instagram, Facebook, and YouTube.
### Zapier Integration
Connect Sequenzy to over 6,000 apps using [Zapier](https://zapier.com). Automate your email marketing workflows by connecting to CRMs, forms, payment providers, and more: no code required.
**Available actions:**
* **Create or Update Subscriber**: Add leads from Typeform, Webflow, or any form tool
* **Add/Remove Tags**: Tag subscribers based on actions in other apps
* **Trigger Events**: Fire custom events to start automations
* **Delete Subscriber**: Handle unsubscribes from other systems
* **Find Subscriber**: Look up subscriber data by email
**Example workflows:**
* Add Typeform respondents as subscribers with their answers as attributes
* Tag Calendly bookings as "booked-call" and trigger a nurture sequence
* Sync Stripe customers with "customer" tag automatically
Navigate to **Settings → Integrations** to see setup instructions, or visit [Zapier's Sequenzy page](https://zapier.com/apps/sequenzy/integrations) to get started.
[View documentation](/integrations/zapier)
### Select All Subscribers for Enrollment
When enrolling subscribers into a sequence, you can now select all matching subscribers at once. A "Select all" checkbox makes it easy to enroll your entire filtered list without clicking each subscriber individually.
### Keyboard Shortcuts for Sequence Builder
Navigate the sequence builder faster with keyboard shortcuts:
* **G** - Open Goals modal
* **S** - Open Enroll Subscribers modal (when sequence is active/paused)
* **,** (comma) - Open Settings modal
Shortcuts are displayed in button tooltips. They respect input focus and won't trigger when you're typing in a text field or when modals are open.
### Send Test Email Button
A new "Send Test" button is now available in the email preview panel. Quickly send a test email to yourself or your team without leaving the editor. Previously, you needed to navigate to the campaign settings to send a test.
### Selective Subscriber Enrollment
When enrolling subscribers into a sequence, you can now choose exactly which subscribers to enroll. The enrollment modal shows each matching subscriber with their "date added" so you can make informed decisions about who to include.
**New features:**
* **Individual selection**: Check/uncheck specific subscribers
* **Date context**: See when each subscriber was added
* **Progress tracking**: Watch enrollment progress with a real-time counter
* **Bulk actions**: Select all or clear selection with one click
This is useful when you want to enroll only recent subscribers or exclude specific contacts from a sequence.
### Bulk Add to List
Select multiple subscribers from the subscribers page and add them all to a list in one action. Previously, you needed to add subscribers to lists one at a time or use the API.
**How to use:**
1. Go to **Subscribers** and select the subscribers you want to add
2. Click the "Add to List" button in the bulk actions bar
3. Choose the target list
4. All selected subscribers are added instantly
### Email Reply Tracking & Conversations
Track and manage replies to your emails with the new conversation inbox. When subscribers reply to your campaigns, sequences, or transactional emails, their responses are captured and organized into threaded conversations.
**Key features:**
* **Automatic reply capture**: Replies to any email are automatically tracked and linked to the original message
* **Conversation threading**: Multiple replies are grouped into conversations with full message history
* **Reply forwarding**: Optionally forward subscriber replies to your team's email address
* **Two-way conversations**: Reply directly to subscribers from the Sequenzy dashboard
* **Reply analytics**: Track reply rates alongside opens and clicks in your metrics
**How it works:**
1. Enable reply tracking in **Settings → Email Tracking**
2. Outbound emails automatically use a trackable reply address
3. When subscribers reply, messages appear in your **Inbox**
4. View conversation threads, reply to subscribers, or forward to your team
**Conversation management:**
* Mark conversations as open or closed
* View all messages in a threaded timeline
* See subscriber profile alongside conversations
* Search and filter by status, date, or subscriber
**Retention:**
Replies are retained based on your configured retention period (default: 30 days). You can adjust this in Settings.
[View documentation](/concepts/reply-tracking)
### Send Time Optimization
Deliver campaign emails at each subscriber's optimal time based on their historical engagement patterns. When enabled, Sequenzy analyzes when each subscriber typically opens emails and schedules delivery accordingly.
**How it works:**
* Enable "Send Time Optimization" when scheduling a campaign
* Sequenzy analyzes each subscriber's open patterns from the last 90 days
* Emails are delivered within a 12-hour window, timed to when each subscriber is most likely to engage
* Subscribers without enough engagement history receive emails at the scheduled time
**Requirements:**
Subscribers need at least 5 email opens in the last 90 days for their optimal time to be calculated. The campaign scheduling UI shows how many subscribers have computed optimal times.
**Analytics:**
After sending, view a delivery distribution chart showing how your campaign was spread across hours. This helps you understand how STO distributed your emails and correlate with engagement metrics.
### Campaign Revenue Attribution
See exactly how much revenue each campaign generates with the new attribution panel. When viewing sent campaigns, you'll now see conversions and tracked revenue directly attributed to that campaign.
**What's included:**
* **Conversions count**: Number of goal conversions attributed to the campaign
* **Tracked value**: Total revenue from attributed conversions
* **Goal breakdown**: See which goals drove conversions (if you have multiple goals)
Attribution uses a 24-hour window from email interaction (open or click) to conversion. This feature works with any goal you've configured, including the default revenue tracking from Stripe, Polar, Paddle, and Dodo integrations.
Navigate to any sent campaign to view its attribution metrics alongside engagement stats.
### Activity-Based Triggers for Sequences
You can now trigger automation sequences based on subscriber activity patterns. Two new trigger types are available in the sequence builder:
**Inactivity Trigger**
Start a sequence when a subscriber hasn't performed a specific event for a set number of days. Perfect for:
* Re-engagement campaigns for inactive users
* Churn prevention when users stop using key features
* Win-back flows for lapsed customers
**Event Frequency Trigger**
Start a sequence when a subscriber performs an event multiple times within a time window. Great for:
* Rewarding power users who complete actions frequently
* Identifying highly engaged subscribers
* Triggering upgrade prompts for heavy usage
**How to use:**
1. Create a new sequence or edit an existing one
2. Click on the trigger node
3. Select "Inactivity" or "Event frequency" from the trigger type dropdown
4. Choose the event to track and configure the time settings
### PostHog Integration
Connect your [PostHog](https://posthog.com) product analytics to trigger email automations based on user behavior. Send emails when users perform (or skip) specific actions in your product.
**What you can do:**
* **Trigger emails from product events**: Feature used, onboarding completed, checkout started
* **Build behavior-based segments**: Users who haven't used feature X in 7 days
* **Re-engage inactive users**: Detect users who stopped using your product
**How it works:**
1. Copy your webhook URL from Sequenzy Settings → Integrations
2. Add it as a Webhook destination in PostHog Data Pipelines
3. Events flow in real-time and trigger your automations
All PostHog events are prefixed with `product.` in Sequenzy (e.g., `product.feature_used`).
[View documentation](/integrations/posthog)
### GitHub @mention in AI Writer
Generate product update emails from your actual commit history and releases. In the AI email editor, type `@github owner/repo` and the AI will pull recent commits and releases to write accurate product update emails.
**Example:**
> "Write a product update email based on @github acme/saas-app from the last month"
The AI will fetch your latest commits and releases, then generate an email highlighting real features and improvements - not generic marketing copy.
### Auth Provider Integrations (Clerk & Supabase)
Automatically sync users from your authentication provider to Sequenzy. Every signup becomes a subscriber: no code required.
**Supported providers:**
* **[Clerk](/integrations/clerk)**: Sync users from Clerk authentication with full profile data
* **[Supabase](/integrations/supabase)**: Sync users from your Supabase database webhooks
**How it works:**
1. Copy your unique webhook URL from Sequenzy
2. Paste it in your Clerk or Supabase dashboard
3. Users automatically sync in real-time
**What gets synced:**
* Email, name, username, and profile data
* User created → subscriber added with `contact.subscribed` event
* User deleted → subscriber unsubscribed automatically
Use these integrations to trigger welcome sequences the moment someone signs up, without writing any code.
Navigate to **Settings → Integrations** to connect your auth provider.
[View Clerk documentation](/integrations/clerk) | [View Supabase documentation](/integrations/supabase)
### Send Transactional Emails via SMTP
You can now send transactional emails using the standard SMTP protocol, making it easy to integrate Sequenzy with any application that supports SMTP (Laravel, legacy systems, etc.).
**Connection details:**
* **Host:** `smtp.sequenzy.com`
* **Port:** 587 (STARTTLS), 465 (SSL/TLS), or 2525 (fallback)
* **Username:** `api`
* **Password:** Your Sequenzy API key
**Features:**
* Same security checks as the REST API (domain verification, rate limiting, bounce protection)
* Branding footer automatically added for free tier
* All emails appear in your dashboard with full tracking
* Up to 50 To recipients per message
```javascript theme={null}
// Node.js example with nodemailer
const transporter = nodemailer.createTransport({
host: "smtp.sequenzy.com",
port: 587,
secure: false,
auth: { user: "api", pass: "ek_your_api_key" },
});
await transporter.sendMail({
from: "you@your-verified-domain.com",
to: "recipient@example.com",
subject: "Hello",
html: "
Sent via SMTP!
",
});
```
[View documentation →](/send-email/smtp)
### Creem Integration
Connect your [Creem](https://creem.io) account to automatically sync revenue data and power your email automations. Creem is an all-in-one payment platform that handles global commerce complexity including payments, subscriptions, and tax compliance.
**What gets synced:**
* **MRR & LTV**: Automatically sync Monthly Recurring Revenue and Lifetime Value for each subscriber
* **Status tags**: `customer`, `cancelled`, `churned`, `saas.monthly`, `saas.yearly`
* **Events**: `saas.purchase`, `saas.cancelled`, `saas.churn`, `saas.refund`, and more
Use these attributes to create powerful segments (e.g., "churned customers with LTV > \$500") and trigger automations based on subscription changes.
Navigate to **Settings → Integrations** to connect your Creem account.
[View documentation](/integrations/creem)
### Goals & Attribution
Track which emails in your sequences actually drive conversions with the new Goals feature. Using last-touch attribution, Sequenzy credits revenue and conversions to the most recent email a subscriber interacted with before converting.
**Key features:**
* **Company-wide goals**: Track overall revenue across all your emails
* **Automation-specific goals**: Measure performance of specific sequences
* **Revenue tracking**: See exactly how much revenue each email generates
* **Per-email breakdown**: Identify your highest-performing emails
**How it works:**
1. Create a goal tied to a conversion event (e.g., `saas.purchase`)
2. Optionally specify a revenue property to track amounts
3. Set your attribution window (default: 24 hours)
4. View which emails drove conversions and revenue
Works automatically with Stripe, Polar, Paddle, and Dodo integrations, or trigger custom events via the API.
[View documentation](/concepts/goals)
### Polar, Paddle, and Dodo Payments integrations
You can now connect three new payment providers to automatically track revenue events and sync subscriber data:
* **[Polar](/integrations/polar)**: For open source developers and digital product creators
* **[Paddle](/integrations/paddle)**: Complete payments infrastructure for SaaS with built-in tax compliance
* **[Dodo Payments](/integrations/dodo)**: Simple payments for indie developers
All three integrations work the same way as Stripe: connect your account, set up webhooks, and Sequenzy automatically tracks purchase events, cancellations, refunds, and more. Use these events to trigger automations like onboarding sequences, win-back campaigns, or churn prevention emails.
**Tracked events:**
* `saas.purchase`, `saas.purchase_monthly`, `saas.purchase_yearly`
* `saas.cancelled`, `saas.churn`, `saas.payment_failed`, `saas.refund`
Navigate to **Settings → Integrations** to connect your payment provider.
### A/B test sequences
You can now add A/B test steps to your automation sequences. Split your subscribers into two groups and send different email variants to measure which performs better.
### A/B test email campaigns
You can now run A/B tests on your email campaigns to optimize for better engagement. Split your audience and test different subject lines, email content, or sender names to see which version performs best.
**How it works:**
1. Create an A/B test step in your automation flow
2. Set the split percentage (e.g., 50/50 or 70/30)
3. Design variant A and variant B emails
4. Monitor results to see which variant wins on opens, clicks, or conversions
Use A/B testing to continuously improve your email performance with data-driven decisions.
### Tags on Stripe sync
When syncing revenue data from Stripe, Sequenzy now automatically applies tags based on subscription status:
* **Status tags**: `customer`, `trial`, `past-due`, `cancelled`, `churned`
* **Billing interval tags**: `saas.monthly`, `saas.yearly`
This makes it easy to segment and target customers based on their subscription state (e.g., find all churned yearly customers, or trial users approaching conversion).
### Quick filters for subscribers
Added more quick filter options to the subscribers list, making it faster to find the subscribers you're looking for. Filter by engagement status, subscription state, and more with a single click.
### Revenue data in subscriber details
The subscriber detail page now displays revenue information (MRR and LTV) directly, giving you a complete view of each subscriber's value without leaving the page.
### Transactional email events linked to subscribers
Transactional emails sent via the API are now properly linked to subscribers by email address. This means email events (sent, delivered, opened, clicked) now appear in the subscriber's activity history, giving you complete visibility into all communications with each contact: both marketing and transactional.
### Embeddable signup form widget
You can now generate a customizable HTML signup form to embed on any website. Navigate to **Settings → Embed Widget** and click on the Form widget to access the visual builder.
**Customization options:**
* **Form Style**: Choose between stacked (button below) or inline (button side-by-side) layouts
* **Placeholder Text**: Customize the email input placeholder
* **Font Color**: Set the input text color
* **Button Text & Color**: Customize the submit button appearance
* **Success Message**: Configure the confirmation message with custom font color and size
* **List Targeting**: Add subscribers to specific lists or all lists
The generated HTML is self-contained with inline styles: just copy and paste into any website. No JavaScript SDK required.
[View documentation →](/widgets/signup-form)
### Email deliverability checker
While editing any email, you'll now see a real-time deliverability score next to the Desktop/Mobile buttons. This rule-based checker analyzes your email content and predicts inbox placement without any AI calls.
**What it checks:**
* **Subject line** (25% of score): Length, spam trigger words, excessive caps, punctuation, emoji usage
* **Preview text** (15% of score): Presence, length, whether it duplicates the subject
* **Content** (40% of score): Text-to-image ratio, link count, unsubscribe link, spam triggers
* **Technical** (20% of score): Sender address, reply-to configuration
**Scoring:**
* **A (90+)**: Likely lands in Primary inbox
* **B (80-89)**: Good deliverability, may land in Promotions
* **C-F (under 80)**: Review flagged issues before sending
Click the score pill to see a detailed breakdown and specific issues to fix.
### Stripe revenue sync
When you connect your Stripe account, Sequenzy now automatically syncs revenue data for all your subscribers. This adds two key attributes to each subscriber:
* **mrr**: Monthly Recurring Revenue (normalized from any billing interval)
* **ltv**: Lifetime Value (total payments minus refunds)
You can use these attributes to create powerful segments like "high-value customers" or "churned subscribers with high LTV", and trigger automations based on subscription changes.
A manual "Sync Revenue" button is also available in Settings → Integrations to re-sync data at any time.
### Add subscribers to specific lists via API
When creating subscribers via the API, you can now specify which lists they should be added to. Pass an array of list IDs in the `lists` parameter to add the subscriber to specific lists. If `lists` is omitted or empty, the subscriber will be added to all lists.
```json theme={null}
{
"email": "user@example.com",
"lists": ["list_abc123", "list_def456"]
}
```
### Dynamic segments
Segments now support filtering by email engagement metrics like opens, clicks, and delivery status. Create segments such as "subscribers who opened any email in the last 30 days" or "subscribers who haven't clicked in 90 days" to target your most (or least) engaged audience. Custom event filtering is coming soon.
### AI-powered segment generation
You can now describe the audience you want to target in plain English, and our AI will generate the segment filters for you. Simply click the AI button in the segment builder, describe your target audience (e.g., "subscribers who opened an email in the last 30 days but haven't clicked"), and the AI will create the appropriate filter rules.
### Geographic distribution map
The Metrics page now includes a geographic distribution section with an interactive world map. See where your subscribers are engaging with your emails by country, with metrics including opens, clicks, open rate, and click rate. Hover over any country to see detailed engagement data.
### Filter metrics by email type
You can now filter your Sent Emails and Metrics pages by email type to see how different types of emails perform:
* **Campaigns**: Bulk marketing emails sent to subscriber lists
* **Transactional**: One-off emails triggered by your application (order confirmations, password resets, etc.)
* **Sequences**: Automated emails from your automation workflows
This helps you understand engagement patterns across different use cases and optimize each type independently.
### File attachments for transactional emails
You can now send file attachments with your transactional emails. Attachments can be provided as Base64-encoded content or as URLs that we fetch on your behalf.
* **Base64 content**: Include the file content directly in your API request
* **URL-based**: Provide a URL and we'll fetch the file when sending
* **40MB limit**: Maximum total attachment size per email
* **Any file type**: PDFs, images, documents, and more
```json theme={null}
{
"to": "user@example.com",
"slug": "invoice",
"attachments": [
{
"filename": "invoice.pdf",
"path": "https://example.com/invoices/123.pdf"
}
]
}
```
### Multiple recipients
Transactional emails now support multiple To recipients.
* **Multiple `to` addresses**: Send to up to 50 recipients at once
* **Automatic deduplication**: Duplicate To addresses are handled automatically
```json theme={null}
{
"to": ["user1@example.com", "user2@example.com"],
"slug": "order-confirmation"
}
```
### Added Teams Functionality
Added comprehensive teams functionality allowing you to collaborate with team members. You can now add team members by email, assign roles (admin or viewer), update member roles, and remove team members. Team members can collaborate on campaigns, subscribers, and other resources within your company workspace.
### Custom sender and reply-to for transactional emails
You can now customize the `from` and `replyTo` addresses for transactional emails.
* **Custom from**: Use any verified domain (e.g., `"Notifications "`)
* **Custom replyTo**: Direct replies to any email address
# Account Notifications
Source: https://docs.sequenzy.com/concepts/account-notifications
Get an email when someone subscribes, submits a form, when a campaign finishes sending, and a weekly report every Monday.
Account notifications are emails Sequenzy sends **to you** about your workspace, as opposed to the emails you send to your subscribers.
Four events are available:
| Event | When it fires |
| - | - |
| `new_subscriber` | A contact joins through a form, landing page, popup, the API, or an integration |
| `form_submitted` | Someone submits a form, popup, or landing page, including people already on your list. The email includes the form name and the submitted fields |
| `campaign_completed` | A campaign finishes sending, with its final sent and failed counts |
| `weekly_report` | Monday morning, summarizing the previous week against the week before. Only for weeks in which the workspace sent more than 10 emails |
## Modes
Each notification is set independently:
* **`instant`** - one email per occurrence
* **`daily`** - one summary per day, listing who joined (new subscribers only)
* **`weekly`** - one report per week (weekly report only)
* **`off`** - no email
The three per-occurrence notifications default to `off` - turn on the ones you want in **Settings → Notifications**. The weekly report defaults to `weekly` for the workspace owner; turn it off there if you would rather read the numbers on the **Analytics** page. Invited members (admins and viewers) default to `off` and can turn it on for themselves.
`form_submitted` and `campaign_completed` do not support `daily`. Each submission and each campaign finish is its own event, so a daily rollup would almost always be a list with a single row in it. `weekly_report` supports only `weekly` and `off`.
Use **Form submitted** when a form is a lead (quote request, contact form, demo request) and you need an email every time, not only the first time that address appears. **New subscriber** is the list-growth alert: it still fires only when a contact is created.
To protect your inbox and Sequenzy's system sender from abusive traffic on public forms, instant form-submission notifications stop after 50 accepted submissions per workspace per UTC day. Submissions continue to be processed normally; only the account-notification email is suppressed until the next UTC day.
## What does not trigger a new-subscriber notification
**Imports never notify.** Uploading a CSV or running a migration creates thousands of contacts, and none of them produce an email. This holds even when you turn **Enroll in sequences** on for the import - that setting decides what happens to the contacts, not whether you hear about each one. The same applies to an integration's initial backfill sync and to events replayed from more than an hour in the past.
For a single contact created through the API, CLI or MCP, a supplied `createdAt` at most one hour old still qualifies for a new-subscriber notification. Older signup dates do not notify on creation; a later double opt-in confirmation can still notify. This does not change sequence enrollment defaults or the rule that bulk imports do not notify on creation.
Everything that represents a real-time signup counts, whether or not it enrolls the contact in sequences: form and popup submissions, landing pages, the subscribers and events APIs, tagging a brand-new address, payment-provider and plugin webhooks, and adding a contact by hand from the dashboard.
**With double opt-in on, you are notified when the contact confirms, not when they sign up.** Someone who requests the confirmation email and never clicks it never produces a notification, so what lands in your inbox matches who actually joined your list. A confirmation is a real person clicking a link, so it notifies even if the pending contact originally arrived in an import.
## The daily cap
If you are set to `instant` and your workspace passes **50 signups in one UTC day**, per-signup emails stop for the rest of that day. You get one email explaining the switch, and the remainder arrive as a single summary.
This keeps a viral day from turning into hundreds of emails. Nothing is lost - the summary reports the true total for the day, including the signups that were not sent individually.
## The weekly report
Every Monday from 08:00 UTC, each workspace that sent more than 10 emails during the previous Monday-to-Sunday week gets one short email. It fits on one screen:
* One row of numbers against the week before: sent, open rate, click rate, and revenue (or new subscribers for workspaces that track no revenue)
* **Worth a look**: up to two sequences whose open rate, click rate, unsubscribe rate, or attributed revenue got meaningfully worse, each linked to the sequence. A sequence needs at least 20 sends in both weeks and a change of at least one percentage point and 15% relative before it is called out, so a sequence that sent twelve emails does not read as a collapse
Per-sequence numbers, goal conversions, unsubscribes, and campaign counts stay on the **Analytics** page, which the report links to. If you belong to several workspaces, your two busiest ones share one email.
The 10-send threshold is the whole gate: a workspace that did not send that week gets nothing, however its members' settings are set. That keeps the report out of the inboxes of people who signed up, looked around, and moved on.
By default only the workspace owner receives the report. Teammates who want it turn it on for themselves under **Settings → Notifications**, so inviting a large team never fans one workspace's numbers out to a dozen inboxes unasked.
Numbers come from the same analytics as the **Analytics** page and exclude test sends. If a worker outage delays the Monday run, the report goes out as soon as the scheduler recovers, up to Wednesday; after that the week is skipped rather than sent stale.
## Unusual signup activity alerts
Separately from the notifications you opt into, Sequenzy watches each workspace's public form signup volume. If a day's signups climb far above the workspace's recent baseline - the pattern of a bot scripting a public form (list-bombing) - every non-restricted workspace member gets one security alert email for that day, regardless of their notification settings. The email shows the day's count against the baseline and suggests next steps: review the newest subscribers, pause any welcome sequence on the affected list, and consider enabling [double opt-in](/concepts/double-opt-in) so unconfirmed signups never reach your lists.
## Per person, not per workspace
These settings belong to you, not to the workspace. Each teammate chooses their own, and changing yours never changes anyone else's. Members with **restricted** (link-only) access do not receive account notifications at all.
If you belong to several workspaces, you configure each one separately.
## Changing your settings
Go to **Settings → Notifications**.
```bash theme={null}
sequenzy notifications get
# Switch to a daily summary before a big launch
sequenzy notifications set --new-subscriber daily
# Email every form submission, including returning contacts
sequenzy notifications set --form-submitted instant
# Stop the Monday report
sequenzy notifications set --weekly-report off
# Silence everything
sequenzy notifications set --new-subscriber off --form-submitted off --campaign-completed off --weekly-report off
```
Add `--json` to either command for machine-readable output.
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/notification-preferences" \
-H "Authorization: Bearer API_KEY"
```
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/notification-preferences" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"notificationPreferences": [{"event": "new_subscriber", "mode": "daily"}]}'
```
Both endpoints act on the API key's own user. There is no way to read or change a teammate's notification settings through the API.
Reading requires `account:read`; changing preferences requires `companies:manage`.
Use the `get_notification_preferences` and `update_notification_preferences` tools.
## Sending this somewhere other than email
For Slack, Zapier, or your own service, use an [outbound webhook](/integrations/outbound-webhooks) on the `subscriber.created` event instead. Unlike the notification above, that event fires for **every** created contact, including imports, and carries a `source` field you can filter on.
# Accounts
Source: https://docs.sequenzy.com/concepts/accounts
Model the companies, workspaces and teams your contacts belong to, and message them as one
Accounts are the organizations behind your contacts: the company, workspace or team that a user belongs to in your product. Contacts stay the unit you email; accounts add the layer that B2B products actually bill and support.
With accounts you can:
* keep plan, seats, MRR and trial dates on the organization and use them in every member's segments and emails
* send an event about the organization (trial ending, payment failed, seat limit reached) to the right people in it
* run a sequence once per company instead of once per seat
* see who belongs to which account, with roles, on one page
Accounts are your customers' organizations. They are separate from your
Sequenzy account (`sequenzy account`, the `get_account` MCP tool and `GET
/api/v1/account`) and from the `account:read` API key scope. Account endpoints
use `subscribers:read` and `subscribers:write`; account events also need
`events:write`, and deleting an account needs `subscribers:delete`.
You manage accounts in the **Accounts** tab under **Subscribers**. Nothing about contacts, events or segments changes until your workspace turns Accounts on.
## Turning Accounts on
Your workspace turns Accounts on when you create its first account (in the dashboard, API, CLI or MCP) or run the backfill. It stays on after that, even if you delete every account.
Accounts use `account.*` names in segments and merge tags. If your contacts already have a custom attribute named `account` or `account.`, Sequenzy refuses to turn Accounts on (the API returns `409`) and the Accounts tab lists the attributes to rename. Without this check, segments built on those attributes would stop updating.
## Data model
| Property | Type | Description |
| - | - | - |
| `externalId` | string | Your organization ID. Required, unique per workspace, case-sensitive. |
| `name` | string | Display name. |
| `domain` | string | Primary domain, normalized to a hostname such as `acme.com`. |
| `attributes` | object | Up to 100 custom attributes: plan, seats, mrr, trialEndsAt, usage counters. |
| `memberCount` | number | Maintained by Sequenzy. |
| `lastEventAt` | date | Time of the latest account event. |
Members are contacts with a role: `owner`, `admin` or `member`. A contact can belong to several accounts.
## Getting started
If your events carry an organization id (for example `workspaceId`, `organizationId` or `teamId`), or your contacts carry one as an attribute (for example `account_id` with a `company_name` next to it), open **Subscribers**, then **Accounts**. Sequenzy shows the property it found and previews the biggest organizations it would create, with their contacts. Accounts are named from a matching name property (such as `workspaceName`) or, when there is none, after the work email domain most of their contacts share, so an ID like `zplgap...` with contacts at `leadpages.com` becomes **Leadpages**. Creating them adds each contact to their organization's account in the background. Nothing is sent, and no sequences or sync rules run. The same flow is available from the [API](/api-reference/accounts/organization-ids-detect), `sequenzy accounts detect-org-ids` and the `detect_account_organization_ids` MCP tool.
No organization id? When several contacts share a work email domain, such as seven people at `acme.com`, **Subscribers**, then **Accounts** suggests an account for it. Uncheck any that are not customers and create the rest. Each becomes an account named after the company (external ID and domain `acme.com`), and the domain's contacts become members. Hover a contact count to see who is behind it. Nothing is sent and no sequences start.
Personal and disposable providers (such as `gmail.com` or `yopmail.com`), your own sending domains, domains where a contact already belongs to an account (for example through an organization ID sync) and domains an account already uses are never suggested. The same suggestions are available from the [API](/api-reference/accounts/suggestions-list), `sequenzy accounts suggestions` and the `list_account_suggestions` MCP tool.
Upsert the account, add members, and send account events. The same calls exist in the CLI and MCP tools.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/accounts" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalId": "org_123", "name": "Acme", "domain": "acme.com",
"attributes": {"plan": "pro", "seats": 5, "trialEndsAt": "2026-09-20"}}'
curl -X POST "https://api.sequenzy.com/api/v1/accounts/org_123/members" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane@acme.com", "role": "owner"}'
```
Once Accounts is on, any contact event can carry an `account` reference. The account is created if it does not exist and the contact becomes a member. Before Accounts is on, `account` is ignored exactly as it was before this feature existed, and the response includes `accountIgnored` so you can tell.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane@acme.com", "event": "project_created", "account": "org_123"}'
```
Pass an object instead of a string to set the role or name at the same time: `{"externalId": "org_123", "name": "Acme", "role": "owner"}`.
In the CLI, use `sequenzy subscribers event jane@acme.com --name project_created --account org_123`, or replace `--account` with `--account-json '{"externalId":"org_123","role":"owner"}'` for profile fields and a role. The MCP `trigger_subscriber_event` tool accepts the same string or object in `account`. These options also work with historical events; the timestamp determines whether sequences run.
## Attributes in segments and emails
Account attributes fan out to every member as `account.`. Nothing changes on the contact's own custom attributes; the account values live alongside them.
* **Segments**: filter on `account.plan is pro`, `account.seats greater than 5`, or `account.role is owner` with the existing attribute filter.
* **Merge tags**: `{{account.name}}`, `{{account.domain}}`, `{{account.externalId}}`, `{{account.memberCount}}`, `{{account.role}}` and `{{account.}}` work in campaigns, sequences and previews.
* **Sequence conditions**: see the table below.
| Name | Reads | Use it for |
| - | - | - |
| `account.` | The contact's primary account right now | Segments, merge tags, conditions in any sequence |
| `event.account.` | The account as it was when the triggering event happened | Sequences started by an account event or a contact event with `account` |
Once Accounts is on, the `account.` namespace is reserved for account data. A contact custom attribute named `account` (or `account.`) is still saved on the contact profile but is no longer synced for segments, so it cannot shadow or clear account values.
### Contacts in several accounts
Segments, merge tags and `account.` conditions use one **primary** account per contact: the membership with the highest role (owner, then admin, then member), and the oldest membership when roles tie. The account page and the contact's profile show every membership. Account events still reach the contact through each account they belong to.
## Billing from payment integrations
If Stripe, Paddle, Lemon Squeezy or another payment integration is connected, accounts show the company's billing without any setup. After each payment event or revenue sync, Sequenzy files the paying contact under their account and updates these account attributes:
| Attribute | Value |
| - | - |
| `plan` | Plan name, or plan ID when there is no name |
| `mrr` | Sum of the members' MRR |
| `billing_status` | `active`, `trialing`, `past_due`, `cancelled` or `churned`, best first |
| `billing_interval` | Billing interval of that subscription |
| `trial_ends_at` | Trial end, while trialing |
| `customer_since` | When the first paying member was added as a contact |
| `ltv` | Sum of the members' lifetime revenue, including one-time purchases |
The paying contact joins the account they already belong to, otherwise the one account using their work email domain, otherwise the account whose ID matches an organization ID on the contact (for example Stripe customer metadata `workspaceId`). A contact counts as paying with a payment tag, MRR or lifetime revenue, so one-time buyers are included. They become Owner when the account has none. Like other account attributes, these reach every member as `account.plan`, `account.billing_status` and so on, so a segment on `account.billing_status` includes teammates who never paid. When a workspace turns Accounts on, existing paying customers are filed and rolled up automatically.
## Account events
An account event is about the organization, not one person. Sequenzy records it on the account timeline and delivers it to the chosen recipients as a normal contact event, so sync rules and sequence triggers run per person.
By default only **owners** receive it. Pass `recipients: "all"` when every member should get the email.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/accounts/org_123/events" \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": "trial_ending", "properties": {"daysLeft": 3}, "recipients": "owners"}'
```
| `recipients` | Who receives the contact event |
| - | - |
| `owners` | Default. Owners; falls back to admins when the account has no owner. |
| `admins` | Owners and admins. |
| `all` | Every member. |
| `none` | Nobody. The event is recorded on the timeline only. |
Each delivery carries `event.account.externalId`, `event.account.name`, `event.account.role` and the account attributes, so a sequence triggered by `trial_ending` can say "Acme's trial ends in `{{event.daysLeft}}` days".
Pass `occurredAt` (ISO 8601) to preserve an event’s original timestamp, or omit it for the current time. This is also available as CLI `--occurred-at` and MCP `trigger_account_event.occurredAt`.
Pass `eventId` when your integration may retry. The ID is scoped to the account, so two accounts can use the same ID even when they share members. Retries keep the original recipients and event data, skip completed deliveries, and resume failed work without duplicating sequence enrollments.
## Once per account
Set the sequence's enrollment mode to **matching field** with `account.externalId` to allow one active run per organization across its members. Combined with `recipients: owners`, only one owner enrolls while that account's run is active. A new event can start another run after it finishes; other matching fields retain their per-contact behavior.
## Limits and behavior
* 100 attributes per account; attribute keys up to 100 characters; string values up to 2,000 characters.
* Attribute fan-out and recipient resolution cover up to 5,000 members per account.
* Deleting an account keeps the contacts and clears their `account.*` values. Deleting a contact removes their memberships. If attribute cleanup is temporarily unavailable, Sequenzy retries it automatically.
* Account events are kept for the same retention window as contact events.
## API, CLI and MCP
* REST: [List](/api-reference/accounts/list), [Upsert](/api-reference/accounts/upsert), [Get](/api-reference/accounts/get), [Update](/api-reference/accounts/update), [Delete](/api-reference/accounts/delete), [Members](/api-reference/accounts/members), [Trigger event](/api-reference/accounts/events-trigger), [List events](/api-reference/accounts/events-list). Suggestions: [List](/api-reference/accounts/suggestions-list), [Accept](/api-reference/accounts/suggestions-accept). Organization IDs: [Detect](/api-reference/accounts/organization-ids-detect), [Preview](/api-reference/accounts/organization-ids-preview), [Create](/api-reference/accounts/organization-ids-create), [Job status](/api-reference/accounts/organization-ids-job).
* CLI: `sequenzy accounts list|get|upsert|delete|members|add-member|remove-member|event|events|suggestions|accept-suggestions|detect-org-ids|preview-org-id|create-from-org-id`. See the [CLI reference](/concepts/cli#accounts).
* MCP: `list_accounts`, `get_account_by_external_id`, `upsert_account`, `delete_account`, `list_account_members`, `add_account_member`, `remove_account_member`, `trigger_account_event`, `list_account_events`, `list_account_suggestions`, `accept_account_suggestions`, `detect_account_organization_ids`, `preview_accounts_from_organization_id`, `create_accounts_from_organization_id`, `get_account_organization_id_job`. See the [MCP reference](/concepts/mcp).
# Apple Private Relay
Source: https://docs.sequenzy.com/concepts/apple-private-relay
Deliver to Apple Private Relay subscribers by registering your sending domains with Apple
If your app offers Sign in with Apple, some of your subscribers have an address
like `a1b2c3d4e5@privaterelay.appleid.com` or
`a1b2c3d4e5@private.icloud.com`. Legacy relay addresses can also use
`@icloud.com`. These are real, working mailboxes that forward
to the person's actual inbox - but Apple only forwards mail from senders you
have registered.
Until you register, Apple rejects every message with:
```
550 5.1.1 Relay not allowed for from domain:
```
This looks like an invalid address, but it is not. The mailbox is fine. Your
sending domain simply is not on Apple's allowlist yet.
Do not delete these subscribers. Once you complete the setup below, the same
addresses start delivering. Sequenzy keeps them as temporary bounces
specifically so they survive until you do.
## Register your domains with Apple
Apple validates SPF against the **envelope sender** - the Return-Path domain -
not the From address your recipients see. Those are different domains in
Sequenzy, and registering only the visible one is the most common reason mail
keeps bouncing after you believe the setup is done.
Register every source shown in Sequenzy.
1. In Sequenzy, go to **Settings → Domain** and open your domain.
2. Find the **Apple Private Relay** section. It appears once your audience
contains at least one relay address, and lists the exact domains to
register, including your Return-Path domain (something like
`send.mail.yourdomain.com`).
3. Sign in to your [Apple Developer account](https://developer.apple.com/account/resources/services/configure)
and go to **Certificates, Identifiers & Profiles → Sign in with Apple for
Email Communication**.
4. Under **Email Sources**, add every domain from step 2.
5. Click **(Re)Verify SPF** on each one. Apple requires the domain to publish a
valid SPF record - if your Sequenzy domain is already verified, it does.
Delivery starts on your next send. Apple’s [documented limits](https://developer.apple.com/documentation/signinwithapple/configuring-your-environment-for-sign-in-with-apple)
are up to 32 email sources for an individual developer and up to 100 for an
organization.
## How Sequenzy handles the bounces
An Apple relay rejection for an unregistered sender is recorded as a
**temporary** bounce, labelled *"Apple Private Relay - sending domain not
registered with Apple"*. Unlike a normal bounce, it:
* never adds the address to the global suppression list
* never marks the subscriber as bounced
* never counts toward your account or campaign bounce thresholds
A relay address that the recipient actually deleted (Apple returns
`550 5.1.1 user unknown` for those) is still treated as a permanent bounce and
suppressed normally.
## Finding your relay subscribers
To see how many you have, create a segment with an OR filter for
**Email · contains · `privaterelay.appleid.com`**, **Email · contains ·
`private.icloud.com`**, or **Email · contains · `icloud.com`**. The last match
also includes ordinary iCloud mailboxes, so use it only when that broader
audience is acceptable.
If your app does not use Sign in with Apple, or you do not have an Apple
Developer account, you can exclude that segment from your campaigns. Prefer
that over deleting the subscribers - you keep the contacts if you register
later.
## Via the API
[`GET https://api.sequenzy.com/api/v1/websites/{domain}`](/api-reference/websites/get) (scope `websites:read`) returns the values to register:
```json theme={null}
{
"mailFrom": { "domain": "send.mail.yourdomain.com", "status": "verified" },
"appleEmailSources": [
"send.mail.yourdomain.com",
"yourdomain.com",
"updates.yourdomain.com"
]
}
```
# Campaigns
Source: https://docs.sequenzy.com/concepts/campaigns
Send targeted email broadcasts to your subscriber segments
Campaigns are one-time email broadcasts sent to a segment of your subscribers. Use them for announcements, newsletters, promotions, and any communication that goes to multiple people at once.
Workspaces with the [SMS add-on](/concepts/sms) can also send SMS campaigns: the Campaigns screen gains an Email / SMS switch, and text campaigns follow the same message, recipients, schedule, and review steps. See [SMS campaigns](/concepts/sms#sms-campaigns) for the SMS-specific rules.
When you create an email from a prompt in the dashboard, your first applied AI result saves automatically. Wait for the save confirmation before refreshing. Later edits still use **Save**; if the automatic save fails, your result stays in the editor and you can click **Save** to retry.
## Campaigns vs Sequences
| Aspect | Campaigns | Sequences |
| - | - | - |
| **Timing** | One-time, scheduled | Ongoing, triggered |
| **Trigger** | Manual or scheduled | Automatic (tag/event) |
| **Recipients** | Filtered segment | Individual subscribers |
| **Use case** | Newsletters, announcements | Onboarding, follow-ups |
## Campaign States
```
┌─────────┐ ┌───────────┐ ┌─────────┐ ┌────────┐
│ Draft │ ──▶ │ Scheduled │ ──▶ │ Sending │ ──▶ │ Sent │
└─────────┘ └───────────┘ └─────────┘ └────────┘
│ │ │
│ ▼ ▼
│ ┌───────────┐ ┌──────────┐
│ │ Cancelled │ │ Paused │
│ └───────────┘ └──────────┘
│
▼ (safety review, see below)
┌──────────────────┐ ┌───────────┐
│ Waiting approval │ ──▶ │ Rejected │
└──────────────────┘ └───────────┘
```
| State | Description |
| - | - |
| `draft` | Being created or edited |
| `scheduled` | Queued to send at a specific time |
| `waiting_approval` | Held for [safety review](#safety-review) before it can be queued |
| `rejected` | Safety review declined the send; the reason is returned in `rejectionComment` |
| `sending` | Currently being sent |
| `paused` | Sending was paused manually or by a company-level sending lock |
| `sent` | Completed sending |
| `cancelled` | Manually cancelled before sending |
## Safety Review
Scheduling a campaign can put it in `waiting_approval` instead of `scheduled`.
This is an anti-abuse hold, not an error: nothing is sent while a campaign is in
this state, and the campaign is not lost.
**This most often happens on new accounts.** The checks run against your account
and your sending domain, so a brand-new workspace or a recently registered
domain is the usual reason a first send is held. Once your account and domain
have a track record, later sends normally go straight to `scheduled`.
A campaign is held when any of these apply:
* **New or unverifiable domain.** Your sending domain or your company website
domain was registered in the last 7 days, has no public registration record,
or the website did not respond when we checked it.
* **Content that matches known abuse patterns.** Business-opportunity, guaranteed
income, or high-pressure "act now" language in the subject, preheader, or body.
* **Automated content review.** Campaigns to 10 or more recipients, or
campaigns whose content hits our review watchlist, are checked automatically
before the first send.
Campaigns to fewer than 10 recipients skip the automated content review, so
tests and one-off messages send without waiting. If an account dispatches 5 or
more of these small campaigns within 24 hours, the review applies again to the
rest - splitting one large send into many tiny ones does not avoid it. The
domain and abuse-pattern checks above always run.
Established accounts that our team has marked as trusted skip these checks
entirely.
### What happens next
Held campaigns are reviewed by the Sequenzy team. When a review finishes:
* **Approved** - the campaign returns to `scheduled` and sends at its original
time. If that time has already passed while it waited, it moves straight to
`sending`.
* **Rejected** - the campaign moves to `rejected` and the reason is available in
`rejectionComment` on `GET /api/v1/campaigns/{campaignId}`, `sequenzy campaigns get `, and the `get_campaign` MCP tool.
The hold is evaluated per schedule call, not stored permanently on the campaign.
Fixing the underlying cause (verifying a domain that now has history, rewriting
flagged copy) and scheduling again is enough - you do not need a per-campaign
exemption.
If you are building an unattended integration, treat `waiting_approval` as a
normal outcome of scheduling. Check `status` in the schedule response, and if
it is `waiting_approval`, log it and poll the campaign instead of retrying the
schedule call. Retrying will not clear the hold.
If your account sends automated campaigns and you keep hitting review, contact
support and ask to have the workspace marked as trusted. There is no API
parameter or dashboard setting that bypasses the review.
## Creating a Campaign
### 1. Choose Recipients
From **Subscribers**, apply filters or select contacts, then choose **Create campaign**. Analytics activity groups offer the same action. Choose a template, use an existing email, start blank or write an AI prompt. The complete matching group across every page is saved directly in the campaign when you create the draft. This does not create a list, change list membership or enroll contacts in sequences. New matching contacts do not join the draft automatically; edit its selected contacts or use a saved segment if you want a changing audience. Email eligibility is checked before sending.
Saved lists and segments keep their existing **Create campaign** and **Create sequence** actions. Unsaved filtered groups offer campaign creation only.
Draft campaign rows show an **Audience set** chip when an audience has been configured. It is not a final recipient count; review the audience before sending.
Use [Create Campaign for a Group](../api-reference/campaigns/from-audience), `sequenzy campaigns from-audience`, or MCP `create_campaign_for_audience` to create a blank draft with the same frozen contact selection. Update its content using the existing campaign tools before reviewing and sending.
Define who receives the campaign:
**All Subscribers**
```
Recipients: All active subscribers
Filters: None
```
**Filtered Segment**
```
Recipients: Filtered
Filters:
- Status is "active"
- Tags contains "customer"
- Tags not_contains "churned"
```
### 2. Select Email Template
Choose or create the email content:
* Use an existing template from your library
* Create new content in the editor
* Duplicate and modify a previous campaign
You can also make individual blocks conditional. In the block settings, use **Conditional Display** to show that block only when a recipient value matches a rule such as `equals`, `contains`, `exists`, or numeric comparisons.
Conditions are not limited to profile values: you can also target segment membership, custom events (for example "performed `purchase` in the last 30 days"), tags, lists, subscription status, engagement (opened or clicked recently), Stripe and store purchases, signup date, email provider, and SMS status - the same filters you use to build segments. These rules are checked against live subscriber data when each email is sent.
For an `else` branch, use the Conditional (if/else) block, or duplicate the block and invert the rule on the copy. Attribute and variable conditions use the same merge-tag variable names you already use in emails, but without the `{{ }}` braces.
### 3. Choose Sender
Select who the email appears to come from:
```
From: "Sequenzy Team"
```
### 4. Schedule or Send
* **Send Now**: Queue immediately
* **Schedule**: Pick a specific date and time
* **Spread Over Time**: Distribute sends linearly over 1–72 hours
You now choose these in two layers:
1. **Start time**: Send now or schedule a future start
2. **Delivery strategy**: All at once, Spread Over Time, or Send Time Optimization
### 5. Review and Send
Use the final review step to verify the subject, sender and reply-to identities,
resolved audience, delivery plan, and rendered email. Send a test first when you
have changed personalization or conditional content, then confirm the campaign.
When you click **Send Now** or **Schedule Campaign**, Sequenzy shows **Ready to
send?** with a last check of your email: every link and image is opened to find
broken pages, dead domains and missing images, and the content check lists
anything left to fix, such as placeholder text. Nothing there blocks sending; if
something needs attention the button reads **Send anyway**, and **Edit email**
takes you back to the content.
### Checking your email
The badge in the editor toolbar opens the email check. It groups findings
by links, images, content, subject line, preview text, accessibility, email
clients and sender, and **Show** next to a finding selects the block to fix.
Links and images are checked live as you edit: each one is opened from our
servers and reported as working, broken (page not found, domain doesn't exist,
certificate problem, redirect loop), unreachable, or couldn't verify when a site
refuses automated checks. Without a request, the check also flags links missing
`https://`, mistyped addresses, localhost and staging URLs, placeholder domains
such as `example.com`, link shorteners, and link text that shows a different
domain than the link goes to. Links built from merge tags that change per
recipient are only checked for valid syntax; `{{company.url}}` links use your
company website.
To run the same check outside the editor, use the
[Check Campaign](/api-reference/campaigns/check) endpoint,
`sequenzy campaigns check`, or the `check_email` MCP tool. Sequence steps and
templates have matching checks.
## Delivery Options
These options refine how a campaign goes out. Choose them in the Schedule step.
### Recurring Campaigns
Scheduled campaigns can repeat automatically. When scheduling, set **Repeat**
to "Every week" or "Every month" and the campaign becomes a recurring
template:
* At each run time, Sequenzy duplicates the template into a normal one-shot
campaign and sends it
* The audience is re-evaluated at every run, so new subscribers who match the
list or segment are included and removed ones are skipped
* Each run gets its own campaign (named after the template with the run date)
with its own stats
* The email content is snapshotted per run - you can edit the template between
runs, and the next run picks up your changes without altering past runs
* Cancel the recurring campaign to stop the series; runs already sent are
unaffected
Recurring campaigns pair well with dynamic product lists: a repeat block with
**Recommended products** and **Rotate between sends** turned on sends each
subscriber a fresh selection every month without repeating what they already
saw.
If the platform is briefly unavailable at a run time, the run starts as soon
as it recovers. Runs missed by more than the full interval are skipped, not
sent in a burst.
### Spread Over Time
Enable Spread Over Time to distribute your campaign sends gradually over a configurable window, protecting your sender reputation during warmup.
**How it works:**
1. Choose when the campaign should start sending
2. Select "Spread over time" as the delivery strategy
3. Use the slider to choose a delivery window (1-72 hours)
4. The first batch sends at the chosen start time
5. Remaining batches are spaced evenly across the window
**When to use:**
* Warming up a new sending domain
* Sending your first large campaign (1,000+ recipients)
* You've sent fewer than 10,000 emails total (Sequenzy will recommend this automatically)
**Example:**
```
Campaign start: Tomorrow at 9:00 AM
Spread window: 12 hours
Subscribers: 1,200
Chunks: 5
Chunk 1: Sent at 9:00 AM
Chunk 2: Sent at 12:00 PM
Chunk 3: Sent at 3:00 PM
Chunk 4: Sent at 6:00 PM
Chunk 5: Sent at 9:00 PM
```
Spread Over Time and Send Time Optimization are mutually exclusive. You can
only enable one per campaign.
### Recipient Limit
Turn on **Recipient limit** on the schedule step to send to only part of the
audience. Enter the maximum number of recipients and Sequenzy keeps the first
that many matching subscribers, ordered by subscriber ID, after every
audience rule, exclusion and suppression is applied.
**When to use:**
* Ramping up a new sending domain a few hundred contacts per day
* Soft-launching an offer or price change to a small group before the full send
* Working through a large import in daily batches
* Capping send-quota spend on a campaign whose segment keeps growing
The review step shows the capped count ("First 100 of 950 recipients") and the
send resolves the cap when it starts, so a segment that grows in the meantime
still sends to at most the number you set. If you pause and resume a capped
campaign, recipients already reached count against the limit. Duplicated
campaigns and recurring runs keep the limit. For A/B tests, allow at least one
recipient per variant plus one for the winner. Invalid enabled limits block
sending; turn off the limit explicitly to send to the whole audience.
To send the next batch, duplicate the campaign and add an audience filter such
as **Email sent was not sent ever** so contacts who already received it are
excluded, then set the next day's limit. The same field is available as
`maxRecipients` in the [API](/api-reference/campaigns/schedule), the CLI
(`--max-recipients`) and MCP.
### Pauses and Sending Locks
A manual campaign pause stores the pause on that campaign. You can resume it
from the campaign page, and Sequenzy uses the saved spread setting unless you
choose a new one on resume.
Company sending pauses and account bans act as parent sending locks. When the
lock engages, every campaign that is mid-send is paused automatically with the
label **Paused with company sending** (or **Paused with the account** for a
ban), so no further chunks are attempted while the lock is active. Scheduled
campaigns keep their schedule and show **Sending paused** until the lock
clears.
When the lock is removed, those campaigns resume automatically on the normal
resume path: remaining recipients continue at the same pacing as before the
pause, using the saved spread setting. Scheduled campaigns that came due during
the pause start; future scheduled campaigns wait for their scheduled start.
You cannot resume one of these campaigns on its own while the company lock is
still active; the campaign page, API, CLI and MCP explain which lock applies.
An admin restoring sending can leave those campaigns paused instead. They
then stay paused until you resume them from the campaign page, API, CLI or
MCP. Campaigns you manually paused before the company lock engaged remain
paused when company sending is restored.
### Send Time Optimization
Enable Send Time Optimization to deliver emails at each subscriber's optimal time based on their engagement patterns.
**How it works:**
1. Toggle "Send Time Optimization" on the schedule step
2. Sequenzy analyzes each subscriber's historical open times (last 90 days)
3. Emails are delivered within a 12-hour window at each subscriber's optimal hour
4. Subscribers without enough data receive emails at the scheduled time
**Requirements:**
* Subscribers need at least 5 email opens in the last 90 days
* The UI shows how many subscribers have computed optimal times
**Example:**
```
Campaign scheduled: 9:00 AM
12-hour window: 9:00 AM - 9:00 PM
Subscriber A: Usually opens at 2:00 PM → Sent at 2:00 PM
Subscriber B: Usually opens at 6:00 PM → Sent at 6:00 PM
Subscriber C: Not enough data → Sent at 9:00 AM (scheduled time)
```
After sending, view the delivery distribution chart to see how STO spread your campaign across hours.
### Recipient's Local Time
Deliver a scheduled campaign at the same wall-clock time in every recipient's own timezone - "send when it's 8pm for the customer".
**How it works:**
1. Store a timezone on your contacts (see [Subscribers](/concepts/subscribers)) - via the API `timezone` field, a CSV import, the MCP tools, or the contact page
2. On the schedule step, pick a date and time and choose "Recipient's local time" as the delivery strategy
3. Each contact receives the campaign when their local clock reaches the scheduled time. Timezones ahead of yours are delivered earlier - processing starts when the scheduled time first occurs on Earth (UTC+14)
4. Contacts without a stored timezone receive the campaign at the scheduled time in your timezone, and timezones whose local time already passed when processing starts receive it immediately
**Notes:**
* The option is available once at least one recipient has a known timezone; the schedule step shows how many contacts will get local-time delivery
* Not combinable with repeats, Spread Over Time, or Send Time Optimization
* Also available from the [API](/api-reference/campaigns/schedule) (`sendInRecipientTimezone` + `scheduledTimezone`), the CLI (`campaigns schedule --in-recipient-timezone --scheduled-timezone`), and the MCP `schedule_campaign` tool
**Example:**
```
Campaign scheduled: 8:00 PM in New York
Subscriber A (America/New_York): Sent at 8:00 PM New York time
Subscriber B (Asia/Tokyo): Sent at 8:00 PM Tokyo time (13 hours earlier)
Subscriber C (no timezone stored): Sent at 8:00 PM New York time
```
## Filtering Options
### Status Filters
| Filter | Description |
| - | - |
| `is subscribed` | Only active subscribers |
| `is_not subscribed` | Exclude active (rare use) |
### Tag Filters
| Filter | Description |
| - | - |
| `contains [tag]` | Has the specified tag |
| `not_contains [tag]` | Does not have the tag |
**Example: Target customers, exclude churned:**
```
Tags contains "customer"
Tags not_contains "churned"
Tags not_contains "unsubscribed"
```
### Email Filters
| Filter | Description |
| - | - |
| `contains [text]` | Email contains text |
| `not_contains [text]` | Email does not contain text |
| `is [address]` | Email is exactly this address |
| `is_not [address]` | Email is anything but this address |
`is` and `is_not` match the full address and ignore case, so they are the right
choice for explicit allowlists (QA contacts) and exclusions. Use `contains` when
you want a domain or substring.
**Example: Target company emails:**
```
Email contains "@company.com"
```
**Example: Only a fixed list of test contacts:**
```
Email is "qa1@company.com" OR Email is "qa2@company.com"
```
### Date Filters
| Filter | Description |
| - | - |
| `less_than [value]` | Added within/after a value |
| `more_than [value]` | Added before/older than it |
You can use rolling values like `7 days ago`, natural language like `last week`, or fixed dates like `2026-03-01` and `Mar 1 2026`.
Rolling values update automatically as time passes. Fixed dates stay fixed.
**Examples:**
```
Added less_than 30 days ago
Added more_than 2026-03-01
```
**More targeting examples:**
```
Status is "active"
Added less_than 7 days ago
Tags contains "customer"
```
```
Tags contains "trial"
Added more_than Mar 1 2026
```
```
Attribute trial_ends_at lte 3 days from now
Email contains "@company.com"
```
### Name Filters
| Filter | Description |
| - | - |
| `firstName contains [text]` | First name contains |
| `lastName contains [text]` | Last name contains |
## Filter Combinations
Filters are combined with AND logic:
```
Tags contains "customer"
AND
Tags contains "pro-plan"
AND
Added more_than 7 days ago
```
This targets: Pro plan customers who signed up more than a week ago.
## Public Share Links
Every email campaign can be shared through a public "view in browser" link -
useful for showing a campaign to stakeholders, clients, or anyone outside your
audience. Unlike the `{{viewInBrowserUrl}}` merge tag, which serves one
recipient's personalized copy, the share link renders an anonymized version:
a sample contact instead of subscriber data, an inert unsubscribe link, and no
open or click tracking.
Click Share in the campaign editor to open the hosted page in a new tab - the
link is minted automatically the first time, and drafts can be shared too, so
you can circulate a campaign for review before it is scheduled or sent. Links
can also be created via [the API](/api-reference/campaigns/share-link-create),
with `sequenzy campaigns share`, or through the `share_campaign` MCP tool. The
link is a high-entropy URL that only exists once it is created, and it is
revocable via the API, `sequenzy campaigns unshare`, or `unshare_campaign` -
after revoking, the old URL returns 404 immediately and sharing again produces
a different URL.
Transactional emails, sequence emails, and standalone templates can be shared
the same way through their underlying email: use the Share button in the
email editor, [the templates API](/api-reference/templates/share-link-create),
`sequenzy templates share`, or the `share_template` MCP tool. Campaign links
stay campaign-level so a shared A/B campaign always shows the winning
variant.
## Sending Process
When a campaign is sent:
```
1. Campaign marked as "sending"
│
▼
2. Fetch all matching subscribers
(apply filters)
│
▼
3. Split into chunks of up to 500 recipients
│
▼
4. Queue each chunk for processing
│
▼
5. Each chunk:
- Render email with personalization
- Send via email provider
- Record in analytics
│
▼
6. All chunks complete
│
▼
7. Campaign marked as "sent"
```
## Personalization
### Available Variables
Use these in your email content:
| Variable | Description |
| - | - |
| `{{FIRST_NAME}}` | Subscriber's first name |
| `{{LAST_NAME}}` | Subscriber's last name |
| `{{NAME}}` | Full name |
| `{{EMAIL}}` | Email address |
| `{{customAttribute}}` | Any custom attribute |
| `{{subscriber.ATTRIBUTE}}` | Custom attribute, explicit form |
| `{{subscriber.firstName}}` | Same as `{{FIRST_NAME}}` |
| `{{html.subscriber.ATTRIBUTE}}` | Custom attribute rendered as raw HTML |
The `subscriber.` prefix also accepts the contact's core fields -
`{{subscriber.firstName}}`, `{{subscriber.lastName}}`, `{{subscriber.email}}`,
and `{{subscriber.name}}` - so either form works for those. A custom attribute
of the same name still wins. The prefix is scoped to the contact, so it does not
reach company or URL variables: use `{{COMPANY_NAME}}`, not
`{{subscriber.companyName}}`.
### Example
```html theme={null}
Hi {{FIRST_NAME|there}},
As a {{plan}} customer, we wanted to share...
Your account: {{EMAIL}}
```
The `|there` syntax provides a fallback if the name is missing, and
`{{ FIRST_NAME | default: "there" }}` does the same thing.
A fallback hides a misspelled tag: if nothing provides the name, every
recipient gets the fallback, including the ones whose real value is stored -
and the email looks perfectly personalized. Rendering a preview reports these
under `unresolvedMergeTags` with reason `unknown`, so check that list rather
than reading the greeting.
### Raw HTML Attribute Values
Merge tag values are always HTML-escaped, so an attribute holding markup like
`
...
` renders as visible code. When a custom attribute stores trusted,
prebuilt HTML - for example a unique per-recipient body your system generated -
add the `html.` prefix to render it as markup:
```html theme={null}
{{html.subscriber.body_html}}
```
This lets one campaign deliver fully unique HTML per recipient while keeping
campaign-level analytics: store each recipient's body in a custom attribute,
then use a single merge tag as the campaign content. It works in raw HTML
campaign bodies, text blocks, and HTML blocks alike, and
`{{subscriber.subject}}` in the subject line pairs with it for per-recipient
subjects.
Keep these rules in mind:
* Only render HTML you generate or trust. Never route end-user input through a
raw HTML tag; a regular `{{subscriber.attribute}}` stays escaped.
* Injected HTML is sanitized: scripts, inline event handlers, and dangerous
URLs are stripped.
* The prefix only applies in HTML text position. Inside attributes such as
`href` or `src`, the value is validated and escaped like an ordinary tag.
* Store final HTML. Merge tags still present in the injected HTML are
substituted before sending, so resolve your own placeholder syntax first.
The same prefix works everywhere merge tags render - sequences, templates, and
[transactional emails](/concepts/transactional-emails#raw-html-variables),
where it also applies to API-passed variables like
`{{html.prerenderedHtml}}`.
## Tracking
### Open Tracking
When enabled, Sequenzy tracks email opens via a tracking pixel.
```html theme={null}
```
### Click Tracking
When enabled, links are wrapped to track clicks:
```html theme={null}
Click hereClick here
```
You can enable/disable tracking per company in Settings → Tracking.
## Analytics
### When a Campaign Was Sent
Reopening a finished campaign shows the date and time it went out next to the
**Sent** badge. A campaign without recorded pacing reads `Sent `; a paced
one reads `Finished `, because for native sends that timestamp is when the
last recipient was handed off rather than when sending began. Hover or focus the info button
beside it for the recorded send strategy - no pacing recorded, spread evenly
over a set number of hours, or at each recipient's optimal open time - plus the
scheduled time when one was set. Imported campaigns may not include pacing
metadata from their source provider. The scheduled time is not presented as the
actual start, because safety review or queue delays can postpone delivery.
You do not have to open a campaign to check. Each row in the campaign list shows
a relative age, and hovering its **Sent** badge gives the same record - the exact
date, plus the recorded pacing when there is any.
The same record is available outside the dashboard. `sentAt`, `spreadOverHours`,
`sendTimeOptimization`, and `sendTimeWindowHours` all come back from
[List Campaigns](/api-reference/campaigns/list),
[Get Campaign](/api-reference/campaigns/get), the `sequenzy campaigns list` and
`sequenzy campaigns get` commands, and the `list_campaigns` / `get_campaign` MCP
tools. Change draft pacing with [Update Campaign](/api-reference/campaigns/update)
or apply it when [scheduling](/api-reference/campaigns/schedule). STO is
campaign-only; sequences use a sending window instead.
After sending, view campaign performance:
| Metric | Description |
| - | - |
| **Sent** | Total emails sent |
| **Delivered** | Successfully delivered |
| **Bounced** | Failed to deliver |
| **Opens** | Unique opens |
| **Clicks** | Unique clicks |
| **Unsubscribes** | Opted out from this email |
| **Open Rate** | Opens / Delivered |
| **Click Rate** | Clicks / Delivered |
### Revenue Attribution
If you have [goals](/concepts/goals) configured, sent campaigns show revenue attribution metrics:
| Metric | Description |
| - | - |
| **Conversions** | Goal conversions attributed to this campaign |
| **Revenue** | Total tracked value from attributed conversions |
Attribution uses your workspace's attribution window (24 hours unless you change it; campaign goals default to 7 days). See [Goals](/concepts/goals) for the window settings and supported billing integrations.
### Tracking Over Time
View how engagement changes:
```
Day 1: 45% opens, 12% clicks
Day 2: 52% opens, 15% clicks
Day 7: 58% opens, 17% clicks
```
Most engagement happens in the first 48 hours.
## Resending to Non-Openers
After a campaign finishes sending, you can resend it to everyone in the same audience who didn't open it. This is a simple way to recover opens - a second touch, often with a fresh subject line, reaches the people who missed the first send.
On a sent campaign, use the **Send to non-openers** button. Before creating anything, it shows an estimate of how many subscribers haven't opened the campaign yet, so you know the size of the resend up front.
Confirming creates a new **draft** named ` (Non-openers)` that:
* Reuses the original campaign's audience (all subscribers, lists, a saved segment, or a filtered audience), and
* Adds a "didn't open this campaign" rule so only people who haven't opened it yet are targeted.
The draft is **not** sent automatically. Review it, tweak the subject line, then schedule or send it like any other campaign. Manual exclusions from the original send are preserved; manually added recipients are dropped so they don't bypass the rule.
Every audience format keeps opener exclusions that manual additions cannot override. Resending a resend also preserves exclusions from earlier campaigns. Your audience is evaluated live, so new members can qualify even if they never received the original. If an older resend draft lacks `excludedCampaignOpenerIds`, recreate it from the original campaign and review the new draft before scheduling.
Resending to non-openers becomes available **6 hours after a campaign finishes
sending**. This gives opens time to register, so the estimate and the resend
audience are accurate. It is not offered for [imported
campaigns](#imported-campaigns), which have no opens in Sequenzy to resend
against.
This is also available outside the dashboard:
* **API**: `POST /api/v1/campaigns/{campaignId}/resend-to-non-openers` (see the [API reference](/api-reference/campaigns/resend-to-non-openers))
* **CLI**: `sequenzy campaigns resend-to-non-openers `
* **MCP**: the `resend_campaign_to_non_openers` tool
## Imported Campaigns
When you move to Sequenzy, you can bring your old campaigns with you as
historical records by creating them with `status: "sent"` and the original
`sentAt` (see [Create Campaign](/api-reference/campaigns/create)). Sequenzy
records campaigns created this way as imported history and labels them
**Imported** in the campaign list and on the campaign page.
Only the email content and the send date come across. Sequenzy never delivered
these campaigns, so it has no recipients, delivery records, opens, clicks or
bounces for them - the campaign page says so in place of the usual analytics
rather than showing zeros. Resending to non-openers is unavailable for the same
reason.
Everything else works normally: you can view and share the email, duplicate it
into a new draft, rename it, and label it alongside the campaigns you send from
Sequenzy.
## Organizing Campaigns and Sequences
Labels group related campaigns and sequences into reusable collections. Open
**Settings → Tags & Labels** to review existing collections, then assign labels from a
campaign or sequence settings panel so related work stays easy to find.
### Create a Label
Give the collection a short, recognizable name. Labels organize content only;
they do not change recipients, enrollment, or sending behavior.
## Campaign Types
### Newsletter
Regular updates to your entire list:
```
Recipients: All active subscribers
Tags: contains "newsletter"
Schedule: Every Tuesday, 10am
```
### Product Announcement
New feature or product launch:
```
Recipients: Filtered
Tags: contains "customer"
Send: Immediately
```
### Promotional
Sales, discounts, special offers:
```
Recipients: Filtered
Tags: contains "active"
Tags: not_contains "enterprise" (they don't need discounts)
```
### Re-engagement
Bring back inactive subscribers:
```
Recipients: Filtered
Tags: contains "inactive"
Added: more_than 60 days ago
```
### Segmented Updates
Different content for different segments:
```
Campaign A: Pro Plan Updates
- Tags: contains "pro-plan"
Campaign B: Starter Plan Updates
- Tags: contains "starter-plan"
Campaign C: Free Tier Updates
- Tags: contains "free-tier"
```
## Best Practices
### 1. Test Before Sending
* Send a test email to yourself
* Check personalization renders correctly
* Verify links work
### 2. Segment Thoughtfully
Don't send everything to everyone:
```
✓ Feature update → Only to customers using that feature
✓ Upgrade offer → Only to free/starter tiers
✓ Renewal reminder → Only to annual subscribers
✗ Everything → All subscribers
```
### 3. Time Your Sends
* **B2B**: Tuesday-Thursday, 10am-2pm
* **B2C**: Evenings and weekends
* **Consider time zones**: Schedule for recipient's local time
### 4. Monitor Deliverability
Watch your metrics:
```
⚠ Bounce rate > 5% → Clean your list
⚠ Complaint rate > 0.1% → Review targeting
⚠ Open rate < 10% → Improve subject lines
```
### 5. Respect Unsubscribes
* Process unsubscribes immediately
* Don't re-add unsubscribed contacts
* Provide easy unsubscribe links
## Campaigns vs Transactional
| Use Campaign | Use Transactional |
| - | - |
| Marketing messages | Order confirmations |
| Newsletters | Password resets |
| Promotions | Account alerts |
| Announcements | Receipts |
Don't use campaigns for transactional messages: they may be delayed or
filtered.
## Control a running campaign A/B test
Your campaign review shows the fields that differ, winning metric, percentage of
the full audience, assigned and sent recipients, total duration and deadline.
Use **Change time** to set total minutes from the original start (15–1,440),
**Change audience** to request a 5–50% sample, or **Finish test early** to choose
which variant the remaining audience receives. Already committed sends keep
their variant. Pausing the campaign pauses delivery; changing settings does not
resume it. You can retry a pending audience change or discard a failed one.
The same workflow is available through [Update A/B Test](/api-reference/ab-tests/update)
and [Select A/B Test Winner](/api-reference/ab-tests/select-winner), CLI
`ab-tests get`, `ab-tests update --test-percentage 30 --duration-minutes 120`,
and `ab-tests select-winner`, and MCP `get_ab_test`, `update_ab_test`, and
`select_ab_test_winner`. Use `--cancel-sample-update` or `cancelSampleUpdate: true`
to discard a failed request; repeat the same target or selected winner to retry.
### Is the difference real?
Under the variant results, the campaign review tells you whether the leading
variant's advantage on the winning metric is statistically significant at 95%
confidence, not yet significant, or still too early to judge (fewer than 30
recipients per variant, or too few opens or clicks). With more than two
variants, the leader has to beat every other variant. Sequence A/B steps show
the same note when metrics are visible on the canvas. The check is
informational: automatic winner selection still picks the highest rate when the
test period ends, so a leader that is not yet significant may be noise. Once a
winner is chosen, the note keeps describing the test itself: it compares only
the emails sent before the winner was picked, not the winner's later sends to
the rest of your audience.
[Get A/B Test Stats](/api-reference/ab-tests/stats) returns the same result as
`significance`, `sequenzy ab-tests stats` prints it, and the MCP
`get_ab_test_stats` tool includes it.
When reducing a campaign sample, only queue jobs whose sends have never started and that have no committed recipients can be withdrawn. Active jobs and pending transport retries finish first, so the minimum can rise before the change applies. Retrying preserves the request; discarding a failed change keeps work already performed, including withdrawn sample jobs. Recipients without committed sends remain eligible for the eventual winner.
## Related
Automated multi-step workflows
Managing your contact list
Segmenting for targeting
Per-user triggered emails
# Cart and Order Blocks
Source: https://docs.sequenzy.com/concepts/cart-and-order-blocks
Show each recipient their real cart or order, a free-shipping progress bar, and product cards with sale badges, ratings, and stock notes
Store emails convert when they show the recipient their own basket, not a generic product list. The commerce blocks in the email editor read the trigger event of a sequence and render it as designed content: the exact lines in a cart, a receipt for an order, and a progress bar toward free shipping. Product cards also gained an automatic sale badge, a star rating, and a low-stock note.
You find all of them in the block menu under **Commerce**, or by typing `/cart`, `/order`, or `/shipping` in the editor.
## Cart items and order summary
The **Cart items** block renders the recipient's line items with a thumbnail, title, variant, quantity, and line price, followed by a subtotal row and a **Return to my cart** button. The **Order summary** block is the same block in its `order` style: it adds a heading such as `Order #{{event.orderNumber}}`, labels the total **Order total**, and points the button at the tracking link.
Both read the trigger event of the sequence. Use them in sequences triggered by:
* `ecommerce.cart_abandoned`, `ecommerce.cart_started`, or `ecommerce.checkout_started` for cart emails
* `ecommerce.order_placed` for order emails (`ecommerce.order_shipped` carries the tracking link but no order number or total, so give the heading and button fallbacks as the Order Confirmation template does)
Campaigns have no trigger event, so the default event source renders nothing in a campaign send. You can use a custom list in subscriber data by changing **Items list** in the **Data** tab. The editor canvas shows sample rows so you can still design the layout; select the block to see the **Sample** chip.
### What happens when data is missing
* **No line items in the event** (for example a campaign, or a cart that emptied before the send): the sent email omits the whole block, including the button.
* **Line items without images** (WooCommerce carts and all order webhooks): Sequenzy fills in the product image and link from your synced catalog when the product IDs match. Unmatched products show a neutral placeholder tile.
* **No checkout URL**: Shopify only emits `event.checkoutUrl` when it can build a cart permalink, so give the button a fallback, for example `{{event.checkoutUrl|https://store.example/cart}}`. The button is hidden when its URL resolves empty.
* **No tracking URL yet**: new order buttons fall back to your store's account page (`/account`). If your store uses a different account route, pick **Custom URL** in the button settings.
### Settings
| Setting | What it does |
| - | - |
| Style | `cart` or `order`. Changes the default total label and button text. |
| Heading | None, the order number from the event, or custom text. Select the block and click the heading on the canvas to edit it. |
| Rows to show | 1 to 12 line items, default 6. |
| Thumbnails, Quantity, Variant | Toggle each column. |
| Total | Shows the total from `event.totalPriceCents`; edit the label or hide the row. |
| Button | Choose where it links: the recipient's cart or checkout, order tracking, or a custom URL. Select the block and click the button text to edit it. |
| Data | Choose the items list and total paths, map the fields inside each item, and override currency. Defaults to `event.lineItems` and `event.totalPriceCents`. |
| Design | Choose which details to show, edit labels, and configure the button. **Dynamic button link** accepts merge tags and a literal fallback URL. |
### Choose your data and item fields
Settings open on **Design**, with a compact event-data summary and links to manage data or preview scenarios. The preview status tells you whether the canvas has a real item list, an empty list, or sample products. Samples are only for designing and are never sent. Select **Edit preview data** to open the email's **Preview** panel. Its **Commerce data** section accepts an item array as JSON, a total in cents, and currency; change your event payload or subscriber data to change what recipients receive.
**Items list** is the path to the array, such as `event.lineItems`, `event.cart.items`, or `basket.items` in subscriber data. **Fields for each item** maps the values inside one entry. For example, if an item is `{"product":{"name":"Ceramic cup"},"count":3,"cost":1200}`, set **Product name** to `product.name`, **Quantity** to `count`, and **Unit price in cents** to `cost`. Do not include the items list prefix or merge tag braces.
When preview items are available, use **Choose** next to a field to select a path with an example value. You can still type a path manually. Each mapping shows the first item's value and flags missing values, invalid quantities, and nonnumeric prices. Decimal prices prompt you to check whether your source uses major units instead of cents; choose **Numeric price units** explicitly: cents (2999 = 29.99) or major units (29.99 = 29.99). Checks examine up to the first 50 items and offer up to 100 fields, nested up to six levels deep.
New cart and order blocks in an event-triggered sequence select `event.items` when that array exists, otherwise `event.lineItems` when available, or the only list of item objects in your event sample. Without an unambiguous sample, store events keep `event.lineItems` and custom events start with `event.items`. Existing saved blocks keep their source. In **Items from your event**, choose a list by its path, item count, and example product. Expand **Custom items path** to enter another path.
Real event rows load automatically into previews, including repeat blocks. Empty arrays stay empty and missing event fields stay missing. **Use latest event data** in Commerce data or **Use latest event items** in Repeat Data replaces your preview overrides with the latest sample, which may belong to another subscriber. It does not change recipient data or fire an event. Recipients use the event that enrolled them; waiting for a later event does not replace that payload.
The Data tab shows the sequence's triggering event when available. **Inspect items payload** expands the current preview value at your items path (up to 20,000 characters). An event name alone does not identify which connected store supplied it.
Blank mappings use the standard keys: `title`, `imageUrl`, `url`, `quantity`, `variantTitle`, and `priceCents`. **Additional item fields** lets you map the formatted `price` fallback and per-item `currency`. If a custom path is missing, it does not silently fall back to a different field. Use **Reset to store event fields** to restore the default list, total, and item mappings.
**Total value** is a separate path in cents, not a sum of the displayed rows. **Currency override** overrides the currency next to the items list; an item's currency still takes precedence for that row. Without a currency, amounts appear without a symbol. Use **Design** to toggle thumbnails, quantities, variants, the total, or the button.
Line totals multiply each item's `priceCents` by its quantity. When an event carries only a formatted `price`, the block shows that value as the unit price.
## Free shipping bar
The **Free shipping bar** block compares the cart total with your threshold and renders one of two messages: "Add \$12.00 more for free shipping" with a proportional bar, or "You've unlocked free shipping!" once the total is at or above the threshold. Set the threshold in your store currency; the bar formats amounts in the currency the event carries, or the one you pick in settings.
Place it above the cart items or the checkout button in cart-abandonment and checkout sequences. Like the cart block, it needs an event total, so campaigns omit it. On the canvas the bar previews a total you choose in settings (60% of the threshold by default).
Two styles are available: **Banner** wraps the bar in a tinted band, **Bar** is the plain track. `{amount}` in the below-threshold copy is replaced with the remaining amount; keep it in single braces, it is not a merge tag.
## Product card extras
Every product card (small, medium, and large) now supports:
* **Sale badge**: "Save 20%" or "Save \$10", computed from the price and compare-at price. Off for previously saved cards; the product picker turns it on when the catalog product has a compare-at price.
* **Stock note**: a short urgency line under the price, such as "Only 3 left". The product picker fills it from Shopify variant inventory when there are five or fewer units; you can type anything, including merge tags.
* **Rating**: 0 to 5 stars in half steps, with an optional caption such as "128 reviews". There is no review sync yet, so this is copy you enter.
Products rendered through a **Recommended products** or catalog-backed repeat block expose the same data as variables: `{{product.saleBadge}}`, `{{product.lowStockNote}}`, and `{{product.inventoryQuantity}}`.
## Templates
Eight store campaign templates join the gallery, all filled from your synced catalog with the automatic sale badge on product cards: **It's Back** (restock), **Price Drop**, **New This Week**, **Build the Set** (bundle code), **Early Access** (VIP preview with countdown), **Free Shipping Weekend**, **Shipping Cutoff** (holiday deadline), and **Reordered Most** (social proof). Lifecycle templates:
* **Cart Recovery** now opens with the free shipping bar and renders the cart with the cart items block.
* **Order Confirmation** is a new automation-only design for `ecommerce.order_placed`: the order summary, tracking button, three next steps, and a pairs-well-with grid.
* **Gift Guide by Price** splits your catalog into three bands (under 25, under 50, and 50 and up, in your catalog currency), each filled with your own products, cheapest first, and padded with stock tiles when a band has fewer than three.
## API, CLI, and MCP
The blocks are `line-items` and `shipping-progress` in the block schema. `GET /api/v1/email-blocks/line-items`, `sequenzy blocks line-items`, and the MCP `get_email_block_schema` tool return every field with its accepted values; the same holds for `shipping-progress`. Product cards accept `saleBadge` (`none`, `percent`, or `amount`), `stockNote`, `rating`, and `ratingCaption`.
The AI email writer can create both blocks. They only render when the send carries event data, so ask for them in sequence emails rather than campaigns.
To use the same workflow through the API, CLI, or MCP, read the latest event with `GET /api/v1/events/sample?eventName=checkout`, `sequenzy events sample checkout --company --json`, or `get_event_sample`. Choose an array from `sample.properties`, save its `event.` path in the block's `source` and map any nonstandard item keys with `itemFields`. For example, a custom `items` array uses `"source": "event.items"`. Updates through the sequence email endpoints, CLI, and MCP preserve these fields. Omitting `source` keeps the shipped `event.lineItems` default; pass a source explicitly to match the editor's sample-based choice. Render with `variables.event` set to the sample properties; replace it with a fresh sample to reset preview values. See [Sequences](/concepts/sequences) for the sample and render workflows.
## Design and preview scenarios
Choose Compact, Classic or Showcase in **Design**, then adjust image size (32–120px), fit (cover or contain), row spacing (0–32px), type size (12–24px) and price visibility. Defaults preserve existing blocks: 64px images, 12px spacing, theme body type size, cover fit and visible prices. Click a selected cart's image or product name to open its matching control. Heading, total label and button copy remain editable on the canvas; **Edit button settings** opens button controls.
In **Data**, apply suggested recognizable field mappings explicitly. Numeric prices with unknown units require you to choose cents or major units. Existing mappings are preserved. **Preview scenarios** shows current event data, one or many products, an empty cart, missing images and long names at desktop or mobile width. Scenarios use cloned data and never alter saved blocks, event samples or recipients. An empty cart hides the whole block, including its total and button. At widths up to 480px, images, product details and prices stack so larger designs stay readable. Email clients without media-query support keep the desktop table.
The [Preview Cart Items endpoint](/api-reference/email-blocks/assist-cart), `sequenzy blocks preview-cart` command and `preview_cart_items` MCP tool return the same suggestions, presets and scenario data. Save `imageSize`, `imageFit`, `rowSpacing`, `fontSize`, `showPrices`, `priceUnit` and `itemFields` through existing block update workflows. Retrieve saved blocks through their existing detail workflows. To reset optional settings, remove those properties from the replacement block rather than setting them to null. Use the existing render workflow for HTML previews, supplying scenario items at the block's source; desktop/mobile are viewing widths, not saved block settings.
# CLI
Source: https://docs.sequenzy.com/concepts/cli
Manage your email marketing from the command line with the Sequenzy CLI
The Sequenzy CLI lets you manage subscribers, run campaigns, send emails, manage your team, triage inbox replies, operate outbound webhooks, and view analytics directly from your terminal.
It also lets you inspect, supply, and AI-sync localized template variants.
## Installation
```bash theme={null}
# Using npx (no install needed)
npx @sequenzy/cli --help
# Using Bun (no install needed)
bunx @sequenzy/cli --help
# Or install globally
npm install -g @sequenzy/cli
```
Use the scoped package name for one-off runs: `npx @sequenzy/cli ...` or `bunx @sequenzy/cli ...`. The unscoped `sequenzy` package is the TypeScript API library and does not expose a CLI binary.
### Using the CLI with AI agents
If an AI agent will run the CLI for you, also install the Sequenzy skill. It gives your agent a versioned workflow guide for operating the CLI safely:
```bash theme={null}
npx skills add Sequenzy/skills --skill sequenzy-email-marketing
```
Learn more in the [Skills documentation](/concepts/skills).
## Authentication
### Login
```bash theme={null}
sequenzy login
```
This prints a login URL and approval code in the terminal. Open the link in any browser, approve the session, and keep the terminal open until the CLI stores your API key.
This flow also works in headless or agent-driven environments where the CLI cannot launch a browser for you.
### Check login status
```bash theme={null}
sequenzy whoami
```
`whoami` checks local credential state only. To validate the credential that
would actually reach Sequenzy (including a `SEQUENZY_API_KEY` environment
override) and inspect its non-secret identity and permissions, run:
```bash theme={null}
sequenzy account
sequenzy account --json
```
### Logout
```bash theme={null}
sequenzy logout
```
## Commands
Destructive commands (`campaigns delete`, `ab-tests delete`, `ab-tests delete-variant`, `lists delete`, `segments delete`, `tags delete`, `webhooks delete`, and `team cancel-invitation`) ask for confirmation before running. Pass `--yes` (or `-y`) to skip the prompt. When stdin is not interactive - in scripts, CI, or agent-driven runs - `--yes` is required and the command fails without it.
### Subscribers
```bash theme={null}
# List subscribers
sequenzy subscribers list
sequenzy subscribers list --tag vip
sequenzy subscribers list --segment active-users
sequenzy subscribers list --list "Master List" --json
# Without --limit the command walks every page and returns all matches at once.
# Page through a large audience in chunks
sequenzy subscribers list --limit 50 --json
sequenzy subscribers list --limit 50 --cursor NEXT_CURSOR --json
sequenzy subscribers list --limit 50 --offset 50 --json # or --page 2
# Follow pagination.nextCursor until pagination.hasMore is false. The cursor is
# cheaper than --offset because the server does not re-scan rows already walked;
# fall back to --offset when nextCursor is null.
# Offset/page navigation is capped below 1,000,000 skipped matches; keep
# following nextCursor for deeper audiences.
# Audit opt-outs by date
sequenzy subscribers list --status unsubscribed --unsubscribed-after 2026-07-01 --json
sequenzy subscribers list --status unsubscribed --unsubscribed-after 2026-07-01 --unsubscribed-before 2026-08-01
# Each row carries unsubscribedAt, the date the contact opted out. Use it rather
# than updatedAt, which any later tag or attribute write moves. It is null unless
# the contact is currently unsubscribed, and null for contacts imported as
# already unsubscribed - the date filters skip those rows.
# Bare dates use UTC midnight. Datetimes must include Z or an explicit offset.
# Filter by a custom attribute
sequenzy subscribers list --attribute plan --attribute-value pro
sequenzy subscribers list --attribute mrr --attribute-value 50 --attribute-operator gte
sequenzy subscribers list --attribute sample_received --attribute-operator is_not_empty
# Operators: is (default), contains, gt, gte, lt, lte, is_not_empty.
# --attribute cannot be combined with --segment; create a segment for
# compound rules or for is_not / not_contains / is_empty.
# Filter by a custom attribute (poll answers are stored as attributes)
sequenzy subscribers list --attribute plan:pro
sequenzy subscribers list --attribute sample_received: --attribute-operator is_not_empty
# Add a subscriber
sequenzy subscribers add user@example.com
sequenzy subscribers add user@example.com --first-name John --last-name Doe --tag premium --attr plan=pro
sequenzy subscribers add user@example.com --list list_abc123
# Subscriber creation and explicit lists use subscribers:write. When --list and --no-lists are
# omitted, a new contact follows workspace defaults and an existing contact
# keeps its memberships. --no-lists needs no list permission.
# Store a timezone for recipient-local campaign delivery
sequenzy subscribers add user@example.com --timezone America/New_York
sequenzy subscribers add --phone '+14155550123' --sms-consent # phone-only SMS contact
# Matching sequences are enrolled by default.
# Import a contact with their original signup date from another platform
sequenzy subscribers add user@example.com --created-at 2019-03-14T09:30:00Z
# Only ever moves an existing contact's date earlier, and skips sequences by default.
sequenzy subscribers add user@example.com --created-at 2019-03-14T09:30:00Z --enroll-sequences
# Explicitly enroll matching sequences; requires automations:trigger.
# Update a subscriber
sequenzy subscribers update user@example.com --first-name Ana --last-name Silva
# See the custom attributes your contacts use, with value types and examples
sequenzy subscribers attributes
sequenzy subscribers attributes --include-nested --json
# Set typed attribute values. --attr always sends strings; --attrs-json keeps
# numbers, booleans, lists, and strings with leading zeros such as zip codes.
# When both set the same key, --attr wins.
sequenzy subscribers add user@example.com --attrs-json '{"zip":"02134","seats":5,"vip":true}'
sequenzy subscribers update user@example.com --attrs-json '{"seats":6}' --merge
# Set the native phone field (--phone-country reads national-format numbers)
sequenzy subscribers update user@example.com --phone '+390512345678'
sequenzy subscribers update user@example.com --phone '051 234 5678' --phone-country IT --sms-consent
# Changing the number resets SMS consent unless --sms-consent is passed too.
# Get subscriber details
sequenzy subscribers get user@example.com
# Remove a subscriber
sequenzy subscribers remove user@example.com
sequenzy subscribers remove user@example.com --hard # permanent delete
# Trigger a custom event for one subscriber
sequenzy subscribers event user@example.com --name saas.purchase --property plan=pro --property amount=99
# Attach the contact to an account and include its context in the event
# (ignored with a warning until your workspace has Accounts on)
sequenzy subscribers event user@example.com --name project_created --account org_123
sequenzy subscribers event user@example.com --name project_created --account-json '{"externalId":"org_123","role":"owner","attributes":{"plan":"pro"}}'
# Backfill an event that already happened (no sequences run, safe to re-run)
sequenzy subscribers event user@example.com --name saas.purchase --occurred-at 2024-11-02T10:00:00Z --event-id inv_9182
# Give a live event your own ID so a retry records nothing the second time
sequenzy subscribers event user@example.com --name saas.purchase --event-id order_12345
# Reconcile tags across many existing subscribers
sequenzy subscribers tags add --tag derived-churn-risk --emails-file ./backlog.csv
sequenzy subscribers tags add --tag vip --subscriber-id sub_123 sub_456
sequenzy subscribers tags remove --tag legacy-plan --emails-file ./backlog.csv
# Import full CRM records from CSV or JSON, then inspect progress
sequenzy subscribers import ./copper-export.csv --duplicate-strategy merge --list list_abc123 --opt-in-mode confirmed
sequenzy subscribers import-status import_abc123
# --list requires lists:write. Workspace-default opt-in can require
# automations:trigger for active contacts; double_opt_in always requires it.
# Record events for many subscribers from a pipeline export
sequenzy subscribers events-import ./purchases.json
```
`subscribers tags add` and `subscribers tags remove` only touch contacts that
already exist: unknown emails, external IDs, and subscriber IDs are reported,
never created. Tag automations stay off unless you pass `--trigger-automations`
on `add`, which is what historical backfills want. The CLI chunks targets into
500-contact API requests and reports combined totals, so a multi-thousand-row
reconciliation is a single command.
Subscriber imports accept columns such as email, full/first/last name, external
ID, phone, status, and tags. Additional CSV columns become typed custom
attributes. Imports run asynchronously; a `completed` import may still contain
row failures, so check `import-status --json`. Use `--opt-in-mode confirmed`
only when the source CRM contains verified consent. A nonempty `--list` needs
`lists:write`. The data-ingest presets omit that scope, so choose custom
permissions for imports into explicit lists or omit `--list` to keep the
workspace default-list behavior. For `subscribers add`, `subscribers:write`
also covers explicit lists.
`import-status` prints the reason counts behind the skipped and failed totals.
The counts add up to the totals, so nothing is excluded without an explanation.
Pass `--idempotency-key` from pipelines and retry loops so a timed-out import
request can be resent without double-queueing the batch.
`subscribers events-import` records events for many subscribers in one request
from a JSON file (or `--events-json`), up to 25 events per call so synchronous
processing stays bounded. Each event needs a `name`, a source-owned `eventId`,
and an `email` or `externalId`; external-ID-only rows must already identify a
contact. History is classified per contact: every row for that contact must be
more than an hour old, or the whole group uses the live side-effect path.
Derive each `eventId` from source data so retries reuse one receipt and repair
downstream work idempotently.
### Lists
```bash theme={null}
# Create a list
sequenzy lists create "Monday Batch 01" --private
# Add subscribers to a list from an array
sequenzy lists add-subscribers list_123 --email one@example.com two@example.com
# Add subscribers to a list from JSON or CSV
sequenzy lists add-subscribers list_123 --emails-json '["one@example.com","two@example.com"]'
sequenzy lists add-subscribers list_123 --emails-file ./batch-001.csv
sequenzy lists import list_123 --emails-file ./batch-001.csv
# Update a list
sequenzy lists update list_123 --name "Weekly Newsletter" --private
sequenzy lists update list_123 --description "Sent every Monday" --no-private
# Remove subscribers from a list
sequenzy lists remove-subscribers list_123 --email one@example.com two@example.com
sequenzy lists remove-subscribers list_123 --emails-file ./churned.csv
# Delete a list
sequenzy lists delete list_123 --yes
```
Use `--private` to omit a list from individual controls on the hosted subscriber
email preferences/unsubscribe page. Use `--no-private` to make its name and
description subscriber-facing there. List privacy does not override a
subscriber's global unsubscribe.
The CLI splits large files into API-safe batches automatically. CSV files can
use an `email`, `e-mail`, `email address`, or `mail` header; if no email header
exists, the first column is used. Use this command instead of looping over
`subscribers add`; standard API rate limits are 300 requests per minute per API
key and 20 requests per second burst.
`lists remove-subscribers` accepts the same email inputs as `add-subscribers`
(`--email`, `--emails-json`, or `--emails-file`) and only removes list
memberships; the subscribers stay in your audience. Emails that do not match a
subscriber are reported as not found. Deleting a list removes all of its
memberships but keeps the subscribers.
### Send Email
Send transactional emails to one recipient or to a shared recipient list:
```bash theme={null}
# Using a template
sequenzy send user@example.com --template welcome --var name=John
# One email to several recipients, with visible Cc and hidden Bcc
sequenzy send buyer@example.com ops@example.com --cc billing@example.com --bcc archive@example.com --template order-confirmation
# With nested array data for repeat blocks
sequenzy send user@example.com --template order-confirmation --vars-json '{"event":{"items":[{"title":"Pro plan","description":"Monthly subscription"}]}}'
# Using raw HTML
sequenzy send user@example.com --subject "Hello" --html "
Hi!
"
# From a file
sequenzy send user@example.com --subject "Report" --html-file ./email.html
# Pipe HTML and return a JSON receipt for one workspace
sequenzy send user@example.com --subject "Report" --html-file - --company comp_123 --idempotency-key report-123 --json < email.html
# Attach a file or embed a CID image (up to 10 files / 15MB total)
sequenzy send user@example.com --subject "Ticket" --html '' --attachments-json '[{"filename":"ticket.png","path":"https://example.com/ticket.png","contentId":"ticket-qr","contentType":"image/png"}]'
# Disable click tracking to keep iOS/Android universal links working
sequenzy send user@example.com --template welcome --no-track-clicks
# Reuse this same key if the command must be retried
sequenzy send user@example.com --template receipt --idempotency-key order-123-receipt
# Use your own one-click unsubscribe link on a transactional send
sequenzy send user@example.com --template weekly-digest --header "List-Unsubscribe: " --header "List-Unsubscribe-Post: List-Unsubscribe=One-Click"
# Send as a non-default verified brand instead of the company default
sequenzy send user@example.com --subject "StudyBoost test" --html "
Hi
" --from-email hello@studyboost.com --from-name StudyBoost --reply-to support@studyboost.com --reply-to-name "StudyBoost Support"
```
Use `--company COMPANY_ID` to select the workspace with a personal API key.
`--json` writes the complete accepted API receipt to stdout without interactive
output. A queued receipt does not confirm delivery; inspect its `emailSendId`
with `sequenzy email-sends get`.
`--template` takes a transactional API slug and maps it to the API's `slug`
field. Direct `--html` and `--html-file` content maps to the API's canonical
`body` field and requires `--subject`. Template and direct-content flags cannot
be combined. If the recipient email matches a stored subscriber, saved first and
last names fill omitted name variables automatically. Explicit `--var` or
`--vars-json` values take precedence.
Pass several addresses to `send`, or add `--cc` and `--bcc`, to deliver one
email that everyone sees addressed the same way, instead of one email per
address. Each list takes up to 50 addresses. Primary recipients can be
space-separated or comma-separated; for several copy recipients, repeat
`--cc`/`--bcc` or use comma-separated values. An address repeated across the
lists is kept only in the highest-priority one: `to` beats `--cc`, which beats
`--bcc`. Shared recipient lists are transactional only, so `--email-type
marketing` still takes exactly one recipient and rejects `--cc` and `--bcc`.
For an agent or script that may retry, generate one `--idempotency-key` before
the first attempt and reuse it with the same arguments. The key returns the
original send for 14 days; generating a fresh key represents another email.
Repeat `--header "Name: value"` to add email headers (up to 50). A
transactional send can carry your own `List-Unsubscribe` and
`List-Unsubscribe-Post`; marketing sends always use Sequenzy's signed
unsubscribe headers. Headers Sequenzy manages, such as `From`, `To`, `Subject`
and `Reply-To`, are not applied. The send still goes out, and the receipt lists
each header that was not applied with a reason (`ignoredHeaders` in `--json`
output). A `--header` without a name and colon, or a name given twice, is
rejected before anything is sent.
`--no-track-clicks` disables click-link rewriting for a single send, which keeps
iOS and Android universal links working. `--no-track-opens` removes the
open-tracking pixel for a single send. Both flags are opt-out only: they cannot
enable tracking your account has disabled.
When `--from-email` and `--sender-profile-id` are omitted, a template send keeps
its saved From identity and a direct send uses the company default. Pass
`--from-email` to send as an existing verified brand instead, and `--from-name`
if that address has several identities. These flags look up profiles and do not
create them. Use either `--from-email` or `--sender-profile-id`, not both. When
`--reply-to` and `--reply-profile` are omitted, a template send keeps its saved
Reply-To identity and a direct send uses the company default. Pass `--reply-to`
with optional `--reply-to-name`, or `--reply-profile`, not both.
### Statistics
```bash theme={null}
# Overview stats
sequenzy stats
sequenzy stats --period 30d
# Send API and transactional SMTP stats
sequenzy stats --email-type transactional --period 30d
# One saved transactional email (all time, or add --period 30d)
sequenzy stats --transactional password-reset
# Campaign stats
sequenzy stats --campaign camp_abc123
sequenzy stats --campaign camp_abc123 --json
# Per mailbox provider (account-level or campaign stats)
sequenzy stats --mailbox-provider gmail --period 30d
sequenzy stats --campaign camp_abc123 --mailbox-provider yahoo
# Sequence stats, including per-step sent/delivered/opened/clicked counts
sequenzy stats --sequence seq_xyz789
# Per-email metrics: one row per campaign and per sequence email step
sequenzy stats --emails
sequenzy stats --emails --step 4
sequenzy stats --emails --sequence seq_abc123,seq_def456 --step 4 --json
sequenzy stats --emails --email-type sequence --sort openRate --limit 20
# Mail client and device shares of unique opens
sequenzy stats --clients --period 30d
sequenzy stats --clients --campaign camp_abc123 --json
```
`--clients` shows which mail clients and devices opened your emails, as shares
of unique opens. Each send counts once, by the client of its first open. With
`--campaign` it covers the whole campaign; account-level breakdowns default to
90 days and accept `--period`, `--start`/`--end`, and `--email-type`.
`--mailbox-provider` scopes account-level or campaign engagement metrics to one
recipient mailbox provider (for example `gmail`, `microsoft`, `yahoo`,
`icloud`, `orange`). Replies, conversions, and revenue cannot be segmented per
provider and report `0` under this filter.
Sequence stats include a Steps section with each email step's sent, delivered,
opened, and clicked counts in graph order, plus its node ID - that is where to
read how many of a given step went out. They also include a Current Enrollments
section with live active and waiting enrollment-run counts grouped by current
node. These live counts are not constrained by `--period`, `--start`, or
`--end`; use `--json` for the complete `enrollmentCounts.byCurrentNode` array.
`--emails` switches to per-email metrics: one row per campaign and per sequence
email step, each with its own funnel, plus totals across every matching email.
Combine `--sequence` (comma-separated) with `--step` to answer cross-sequence
questions such as "how many step-4 emails went out across these sequences" in one
command. Sort with `--sort` and `--order`, page with `--page` and `--limit`, and
omit `--period` for all-time counts. These counts come from retained event
storage, so unlike `email-sends list` they are not limited to the last 14 days.
To go from a step to its recipients, take the node ID and pass it to
`sequenzy events --sequence --automation-node-id ` for the retained
event stream, or `sequenzy email-sends list --automation-node-id ` for the
last 14 days of delivery rows.
Use `sequenzy email-sends list --search "Welcome" --status opened` to discover
recent deliveries by subject/title, recipient, status, type, or source. Then use
`sequenzy email-sends get ` to inspect one delivery's open/click
timestamps and complete event timeline.
Add `--all` to retrieve every matching page. Export RFC 4180 CSV to stdout with
`--csv`, or write a complete file with
`sequenzy email-sends list --all --csv email-sends.csv`. List and detail output
include dashboard URLs, and the command prints a concrete follow-up using the
first returned delivery ID.
The CSV includes `subscriberId`, `automationNodeId`, and `abTestVariantId`
alongside `recipientEmail`, `subject`, and the sent/delivered/opened/clicked
timestamps, so one command produces a recipient-level delivery matrix for a
whole sequence:
```bash theme={null}
sequenzy email-sends list --sequence seq_abc123 --all --csv sequence-deliveries.csv
```
### Suppressed recipients
When someone reports missing email, start with the suppression list rather than
the delivery history - suppressed recipients are skipped before a send row is
ever created:
```bash theme={null}
sequenzy suppressions list
sequenzy suppressions list --search example.com --json
sequenzy suppressions list --sort status --order desc
```
`--sort` accepts `suppressedAt` (default, newest first), `email`, or `status`.
`--sort status` puts the escalations you can actually clear ahead of the
protected ones. An unrecognized `--sort` falls back to newest first, so the
applied order is printed with the results.
Each row shows why the address is blocked and how far the block reaches.
`global` means the mail provider reported the mailbox as permanently dead and
every workspace is blocked; `workspace` means repeated delivery failures of your
own sends escalated the address for your workspace only. Rows marked
`(protected)` are global invalid-recipient suppressions, workspace hard bounces
without conclusive invalid-inbox evidence, or spam complaints and
cannot be removed by a workspace.
`sequenzy suppressions get ` adds the Amazon SES account-level check for
one exact address, and `sequenzy suppressions remove ` clears only a
workspace soft-bounce escalation and reactivates the matching bounced
subscriber. It never clears a global or Amazon SES account-level suppression.
Transactional template listing supports the dashboard filters and metrics:
```bash theme={null}
sequenzy transactional list --search "password reset" --status active
sequenzy transactional list --sort open-rate --order desc
```
`transactional update` edits a saved template by ID or slug. It is a partial
update, so only the fields you pass change:
```bash theme={null}
sequenzy transactional update password-reset --subject "Reset your password"
sequenzy transactional update password-reset --blocks-file ./reset-blocks.json
sequenzy transactional update password-reset --no-enabled
```
Pass `--name`, `--subject`, `--preview-text` (or `--clear-preview-text`), and
`--enabled`/`--no-enabled` for metadata. Replace the body with either
`--html`/`--html-file` or `--blocks-json`/`--blocks-file`, not both. Disabling a
template with `--no-enabled` stops its sends while keeping the template, its
slug, and its stats - use it instead of deleting when you may want the template
back.
Retire an obsolete template with `transactional delete`, which takes an ID or
slug and frees the slug for reuse:
```bash theme={null}
sequenzy transactional delete password-reset --yes
```
The command asks for confirmation unless you pass `--yes`, and it needs a key
with the `transactional:delete` scope. Past deliveries and their stats are kept -
only the saved template is removed - and the linked email content stays behind as
a reusable template, whose ID the output prints so you can delete it separately.
To stop sends without deleting anything, run
`sequenzy transactional update --no-enabled` instead.
Campaign stats show Poll and NPS answer distributions; to read the respondents
themselves, use `poll-responses`:
```bash theme={null}
# Everyone who answered a campaign poll, newest answer first
sequenzy poll-responses --campaign camp_abc123
# One poll block, as JSON
sequenzy poll-responses --campaign camp_abc123 --block-id poll_1 --json
```
Each row is one respondent's latest answer to one poll block, with their email,
the stored value, the subscriber attribute the answer was saved to, and the
response time - so the counts match `stats --campaign `. Block IDs come from
the `polls` array of `stats --campaign --json`. For a sequence email step,
pass the step's automation node ID as `--campaign`. Do not scan subscribers for
the poll attribute instead: the attribute carries no response time and reflects
the latest answer to any email.
Campaign and saved transactional-email stats include a Clicked Links section with the top
destination URLs, click counts, and each link's share of all link clicks;
saved transactional stats also show complaint, reply, bounce-type, and
human-versus-machine engagement breakdowns. Add `--json` to receive the
structured fields.
### Campaigns
Create a draft for selected contacts or a complete filtered group with `sequenzy campaigns from-audience --selection-file ./selection.json --json`. `--selection-json` accepts inline JSON; a file of `-` reads stdin. Use [Create Campaign for a Group](/api-reference/campaigns/from-audience) for selection shapes and limits. The result separates saved membership from current email eligibility. Edit and schedule the draft separately.
```bash theme={null}
# List campaigns
sequenzy campaigns list
sequenzy campaigns list --status draft
# Page through every campaign
sequenzy campaigns list --limit 100
sequenzy campaigns list --limit 100 --offset 100
# Get campaign details
sequenzy campaigns get camp_abc123
# Inspect rejected campaigns and their reviewer feedback
sequenzy campaigns list --status rejected
# Update a draft campaign reply-to
sequenzy campaigns update camp_abc123 --reply-to support@example.com
# Create a draft campaign
sequenzy campaigns create "Launch" --prompt "Announce the new dashboard"
sequenzy campaigns create "Launch" --subject "Big News!" --preheader-text "A quick launch preview" --html-file ./launch.html
sequenzy campaigns create "Launch" --subject "Big News!" --blocks-file ./launch-blocks.json
sequenzy campaigns create "Founder note" --subject "A quick question" --blocks-file ./note-blocks.json --email-preset minimal
# Create a draft with its audience already saved
sequenzy campaigns create "Launch" --subject "Big News!" --html-file ./launch.html --segment seg_abc123
sequenzy campaigns create "Launch" --subject "Big News!" --html-file ./launch.html --lists list_abc123
sequenzy campaigns create "Launch" --subject "Big News!" --html-file ./launch.html --target-lists-json '{"type":"lists","listIds":["list_abc123"]}'
# Update campaign subject or preheader
sequenzy campaigns update camp_abc123 --subject "Updated subject"
sequenzy campaigns update camp_abc123 --preheader-text "Updated preview"
sequenzy campaigns update camp_abc123 --email-preset minimal
# Retarget or clear a draft's saved audience
sequenzy campaigns update camp_abc123 --segment seg_abc123
sequenzy campaigns update camp_abc123 --clear-target-lists
# Schedule a campaign
sequenzy campaigns schedule camp_abc123 --at "2026-06-01T14:00:00Z"
sequenzy campaigns schedule camp_abc123 --at "2026-06-01T14:00:00Z" --segment seg_abc123
sequenzy campaigns schedule camp_abc123 --at "2026-06-01T14:00:00Z" --lists list_abc123
# Schedule a recurring campaign (repeats weekly or monthly)
sequenzy campaigns schedule camp_abc123 --at "2026-06-01T14:00:00Z" --repeat monthly
# Deliver at 8pm in each recipient's own timezone (8pm New York wall time)
sequenzy campaigns schedule camp_abc123 --at "2026-06-01T20:00:00-04:00" --in-recipient-timezone --scheduled-timezone America/New_York
# Return a scheduled campaign to an editable draft
sequenzy campaigns unschedule camp_abc123
# Send test email
sequenzy campaigns test camp_abc123 --to me@example.com
# Render a campaign to email-ready HTML (read-only, nothing is sent)
sequenzy campaigns render camp_abc123 --out preview.html
sequenzy campaigns render camp_abc123 --subscriber sub_abc123 --out -
# Check a campaign before sending: broken links, placeholders, content issues
sequenzy campaigns check camp_abc123
sequenzy campaigns check camp_abc123 --fail-on error
# Cancel a scheduled, paused, or sending campaign
sequenzy campaigns cancel camp_abc123
# Pause and resume delivery
sequenzy campaigns pause camp_abc123
sequenzy campaigns resume camp_abc123
sequenzy campaigns resume camp_abc123 --spread-over-hours 6
# Duplicate a campaign as a new draft
sequenzy campaigns duplicate camp_abc123
sequenzy campaigns duplicate camp_abc123 --mode ab_test
sequenzy campaigns duplicate camp_abc123 --mode variant --variant-id var_b
# Create or revoke a public "view in browser" link
sequenzy campaigns share camp_abc123
sequenzy campaigns unshare camp_abc123
# Resend a sent campaign to people who didn't open it
sequenzy campaigns resend-to-non-openers camp_abc123
# Attach conversion goals to a one-off campaign
sequenzy campaigns goals create camp_abc123 "Recipient signed up" \
--trigger-type event --trigger-event-name custom.signup \
--attribution-window-hours 168
sequenzy campaigns goals create camp_abc123 "Became a customer" \
--trigger-event-name saas.purchase
sequenzy campaigns goals list camp_abc123
# Delete a campaign
sequenzy campaigns delete camp_abc123 --yes
```
`campaigns list` is paginated. The default page size is 50 and `--limit` is
capped at 100, so a single call is not guaranteed to return every campaign. The
header line reads `Campaigns ( of )`, and when more results
remain the command prints the exact `--offset` to pass next. With `--json` the
response carries a `pagination` object (`limit`, `offset`, `count`, `total`,
`hasMore`); keep advancing `--offset` while `hasMore` is `true` to enumerate the
full set. `sequences list` accepts the same `--limit` and `--offset` flags, and
returns every sequence when both are omitted.
Rejected campaign list and detail output include the reviewer feedback when it
was provided. The same value is available as `rejectionComment` with `--json`.
Scheduling requires a future ISO timestamp and a verified sending domain.
`campaigns schedule` can report `waiting_approval` instead of `scheduled`. That
means the campaign was held for [safety
review](/concepts/campaigns#safety-review), which is most
common on new accounts and recently registered sending domains. It is a
successful result, not an error: nothing sends while the campaign is held, and
running `campaigns schedule` again will not clear it. Scripts should read
`status` from `--json` output and poll `campaigns get ` rather than retry.
With `--repeat weekly` or `--repeat monthly`, the campaign becomes a recurring template: each run is duplicated and sent automatically at the cadence, re-evaluating list or segment membership every time. Use `campaigns unschedule` to stop the series and return the template to an editable draft, or re-schedule without `--repeat` to keep a one-shot send.
With `--in-recipient-timezone`, every contact receives the campaign at `--at`'s wall-clock time in their own stored timezone - "send when it's 8pm for the customer". `--scheduled-timezone` is required and names the IANA zone the `--at` wall time refers to. Contacts without a stored timezone (set with `subscribers add`/`update --timezone`) get the campaign at `--at` in that zone. Not combinable with `--repeat` or `--spread-over-hours`.
`campaigns create` accepts `--prompt` to generate the campaign email, including subject, preview text, and blocks. Use `--preview-text` or `--preheader-text` to set the inbox preview text yourself; with `--prompt`, that explicit value overrides the generated preview. `campaigns create` and `campaigns update` also accept raw HTML or Sequenzy block JSON via `--blocks-json` or `--blocks-file`. Use block input when you need editor-compatible content, conditional blocks, repeat blocks, or [Poll and NPS survey blocks](/concepts/polls). Pass `--email-preset branded|minimal` to set Style > Format for native blocks; this is separate from prompt-generation `--style` and cannot be combined with `--html` or `--html-file`. Applying `minimal` removes standalone logo blocks, and switching back to `branded` generates a new logo unless you send the authored logo block again. Campaign stats render answer distributions and NPS scores automatically; add `--json` to receive the complete top-level `polls` array.
`campaigns create` can also save the audience on the draft: pass `--segment ` to target one saved segment, `--lists ` to target one or more lists, or `--target-lists-json`/`--target-lists-file` for any targeting shape, such as `{"type":"all"}`, `{"type":"lists","listIds":["list_abc123"]}`, `{"type":"segment","segmentId":"seg_abc123"}`, or `{"type":"rules","include":[{"type":"lists","listIds":["list_abc123"]}],"exclude":[{"type":"segments","segmentIds":["seg_abc123"]}]}`. `--segment`, `--lists`, and the target list flags are mutually exclusive. `campaigns update` accepts the same audience flags to retarget a draft, plus `--clear-target-lists` to remove saved targeting again, and `campaigns schedule` accepts them to choose the audience as you schedule; scheduling without them reuses the saved targeting, or falls back to all active subscribers when none was saved. `campaigns create`, `campaigns update`, and `campaigns get` all print the saved audience on the `Audience` line and return it as `targetLists` with `--json`.
`campaigns unschedule` removes a scheduled send and any recurrence, then returns the campaign to `draft` so it can be edited and scheduled again. `campaigns cancel` stops scheduled, paused, waiting-approval, and actively sending campaigns immediately. It skips the confirmation prompt on purpose so you can stop a bad send fast, and it cannot be undone. Only sending campaigns can be paused, and only paused campaigns can be resumed; pass `--spread-over-hours` (1-72) to spread the remaining delivery over a longer window. Sending, scheduled, and paused campaigns must be cancelled before they can be deleted.
`campaigns get` prints the campaign's linked email body on the `Email ID` line and returns it as `emailId` with `--json` (null for SMS campaigns). That is the same record `templates get` reads, so you can design an email in the dashboard, fetch its blocks through the campaign or templates commands, and reuse the ID as `templateId` when creating campaigns through the API.
Pass `--max-recipients ` to `campaigns update` or `campaigns schedule` to send to only the first N matching subscribers (by subscriber id) after every audience and suppression rule, for example `--max-recipients 100` while ramping up a new domain. Omit it on `schedule` to keep the cap saved on the draft, or pass `--clear-max-recipients` to send to everyone. Recipients already reached count against the cap when a paused campaign resumes, and duplicates keep it. `campaigns get` and `campaigns list` print a `Recipient limit` line when a cap is set and return it as `maxRecipients` with `--json`.
`campaigns get` also reports the recorded send pacing on the `Delivery` line - `No pacing recorded`, `Spread over 6h`, or `Optimal send time (12h window)` - and returns `spreadOverHours`, `sendTimeOptimization`, and `sendTimeWindowHours` with `--json`. Imported campaigns may not include pacing metadata from their source provider. Read it alongside the `Sent` line, which is stamped when the last recipient is handed off for native sends, so on a paced send it marks the end of the delivery window rather than the start.
Send Time Optimization is campaign-only. Use `campaigns update --send-time-optimization` with optional `--send-time-window-hours <1-24>` to persist it on a draft, or pass the same flags to `campaigns schedule` for that send. Use `--disable-send-time-optimization` on either command to turn it off explicitly. Enabling STO clears saved spread and recipient-timezone pacing; `--spread-over-hours` or `--in-recipient-timezone` disables STO. Sequences use `sendingWindow`, which is an allowed-hours/days gate rather than per-recipient predicted send times.
`campaigns duplicate` defaults to `--mode campaign`, which copies the campaign and its email. Use `--mode ab_test` to also copy the campaign's A/B test with all variants, or `--mode variant` with `--variant-id` to copy a single variant's content as the new campaign email.
`campaigns share` creates a public "view in browser" link anyone can open - the hosted page renders an anonymized copy with a sample contact, an inert unsubscribe link, and no open or click tracking, so it is safe to forward outside your audience. Rerunning the command returns the same URL until you revoke it with `campaigns unshare`, after which the old URL returns 404 and sharing again mints a different one. `campaigns get` prints the active link on the `Share URL` line and returns it as `shareUrl` with `--json`.
`campaigns check`, `sequences check --node `, and `templates check` run the email check from the editor against saved content, plus live verification of every link and image. The summary lists broken, unreachable and unverifiable links, then each issue with its severity, block ID and rule ID; `--json` returns the full report with per-link status. Pass `--no-links` for a fast check that makes no requests, and `--fail-on error` (or `warning`) to exit with code 1 when issues reach that severity, for CI and scripts. Personalization flags work as on `render`. See [Check Campaign](/api-reference/campaigns/check) for every status and rule.
`campaigns render`, `sequences render`, and `templates render` return the exact email-safe HTML that would be sent, so you can embed a true visual preview in your own dashboard or email builder. The render applies your email theme and font, company branding and footer, localization, block visibility conditions, product recommendation blocks, and merge tags.
Personalize with `--subscriber ` for a stored subscriber, or `--email you@example.com` plus optional `--first-name`/`--last-name` for an ad-hoc contact. Give that ad-hoc contact custom attributes with repeatable `--attr key=value` flags, or `--attrs-json` for values that need to be numbers, booleans, or nested objects, so `{{subscriber.*}}` tags resolve against it. Add merge variables with repeatable `--var key=value` flags, or `--vars-json` in the same way. Only a stored subscriber can resolve every piece of subscriber-scoped content, such as block conditions on stored state, product recommendations, and a working unsubscribe link; an ad-hoc contact can still carry repeatable `--tag` flags to evaluate `tag` conditions, or `--empty-tags` to state explicitly that it carries none. With no contact at all, the email renders for a sample contact and contact-specific merge tags resolve to empty values.
Rendering itself needs only a read scope on the campaign, sequence, or template. `--subscriber` puts that subscriber's details into the HTML, so it additionally requires the `subscribers:read` scope.
Use `--out ` to write the HTML to disk, or `--out -` to stream the raw document to stdout for piping. Without `--out`, the command prints a summary (subject, preview text, locale, personalization, tracking, byte size). `--json` returns the full payload including `html`.
Because an email never shows a raw `{{TAG}}` to a recipient, a tag that does not exist and a tag that is merely blank both come out as an empty string. The summary calls out both, and `--json` returns them as `unresolvedMergeTags`: reason `unknown` means nothing provides that name and it will be empty for every recipient (usually a typo, or a tag carried over from another platform), while `no_value` means the name is recognized but blank for the previewed contact. A tag whose fallback covered an unrecognized name is reported as `unknown` too - `{{ subscriber.frstName | default: "there" }}` greets everyone as "there", including the contacts whose first name is stored, and reads as ordinary copy in the output. A name is only called `unknown` when the render had a source to check it against. Without the contact's attributes nothing is checkable at all - a bare `{{plan}}` reads the same attribute map as `{{subscriber.plan}}` - so pass `--subscriber`, or `--email` together with `--attr`/`--attrs-json`, to catch typos. Beyond that, `{{event.*}}` needs sample event properties, which you pass as `--vars-json '{"event": {"itemCount": 3}}'`, since a real send fills those from the enrolling event, and `{{recommendedProducts.*}}` needs `--subscriber` and a catalog with something to recommend. `templates render` on a transactional email needs `--var`/`--vars-json`, because its tags are filled from the `variables` of each send call and nothing marks them as such. Without those, the tags come back as `no_value` rather than being reported as typos. An optional attribute the previewed contact never had set is kept off the typo list by checking the names other contacts in your account carry, which needs the `subscribers:read` scope on the key you are logged in with.
```bash theme={null}
sequenzy campaigns render camp_123 --email you@example.com --attr plan=pro --json
```
Conditional blocks need the same care. The condition fields that read stored subscriber state - `tag`, `segment`, `list`, `status`, `event`, purchases, and engagement - are evaluated per recipient at send time, so a render can only check them for `--subscriber`, or, for a `tag` condition, an `--email` contact carrying repeatable `--tag` flags or `--empty-tags`. A condition the render cannot check is rendered as false, exactly as it would look for a contact who genuinely does not match, so the summary lists those under "Conditions not evaluated" and `--json` returns them as `unevaluatedConditions` with a reason and a hint. An empty list means the branches in the HTML are the branches that contact would actually receive.
```bash theme={null}
sequenzy campaigns render camp_123 --email you@example.com --tag extended
```
`sequences render` on a step downstream of a create-discount step fills `{{discount.code}}` and the other `{{discount.*}}` tags from that step's real configuration, with a placeholder `TEST-CODE` for the code itself, so discount copy can be checked before anyone is enrolled.
Links stay clean by default. Pass `--tracking` to apply your company's auto-UTM decoration as a real send would. Per-send click redirects and the open pixel are added at send time against a real email send record, so they never appear in a render.
`campaigns resend-to-non-openers` creates a draft that targets the same audience as a sent campaign plus a "didn't open this campaign" rule, so only people who haven't opened it yet receive the resend. It's available 6 hours after the campaign finishes sending (to give opens time to register) and never for imported already-sent campaigns, which have no opens in Sequenzy. The response includes an estimate of how many subscribers haven't opened the original. The draft is not sent automatically - review it, then schedule or send it.
Every audience format keeps opener exclusions that manual additions cannot override. Resending a resend also preserves exclusions from earlier campaigns. Your audience is evaluated live, so new members can qualify even if they never received the original. If an older resend draft lacks `excludedCampaignOpenerIds`, recreate it from the original campaign and review the new draft before scheduling.
### Landing Pages
```bash theme={null}
# List landing pages
sequenzy landing-pages list
sequenzy landing-pages list --json
# Create a draft from a built-in template
sequenzy landing-pages create "Product Waitlist" --template waitlist
sequenzy landing-pages create "Launch Page" --template launch --slug launch
# Get or edit a page
sequenzy landing-pages get lp_abc123
sequenzy landing-pages stats lp_abc123 --period 30d
sequenzy landing-pages update lp_abc123 --name "Updated Waitlist" --slug waitlist
# Duplicate a page into a new draft
sequenzy landing-pages duplicate lp_abc123
sequenzy landing-pages duplicate lp_abc123 --name "Spring Sale v2" --slug spring-sale-v2
# Publish or unpublish
sequenzy landing-pages publish lp_abc123
sequenzy landing-pages unpublish lp_abc123
# Connect or verify a custom landing page domain
sequenzy landing-pages connect-domain pages.example.com
sequenzy landing-pages update-domain-settings --verify
sequenzy landing-pages connect-domain offer.example.com --landing-page lp_abc123
sequenzy landing-pages connect-domain example.com --landing-page lp_abc123
sequenzy landing-pages update-domain-settings --landing-page lp_abc123 --verify
sequenzy landing-pages remove-domain lp_abc123
# Delete a page
sequenzy landing-pages delete lp_abc123
```
`landing-pages create`, `update`, `publish`, and `unpublish` can accept full builder JSON with `--content-json` or `--content-file`. In that JSON, button and pricing CTA URLs accept in-page anchors such as `#form`, and `theme.sectionAnimation` plus `theme.sectionAnimationSpeed` set the published page's scroll reveal. Video blocks take a pasted YouTube URL in `url`; other providers and direct video files are rejected. Blocks in the `top` slot render as a full-width band above the hero. `landing-pages duplicate` copies an existing page into a new draft with its own slug and stats. Built-in template keys include `from-scratch`, `waitlist`, `lead-magnet`, `launch`, `demo-request`, `webinar`, `newsletter`, `product-hunt`, `pricing-offer`, `agency-lead-gen`, and `feature-announcement`.
Custom landing page domains need a CNAME record pointing to
`pages.sequenzydns.com`; run `update-domain-settings --verify` after DNS
propagates. Omit `--landing-page` for the shared workspace domain, or provide it
for a hostname dedicated to one page.
### Saved Forms
```bash theme={null}
# List saved forms; JSON includes the complete current content and blocks
sequenzy forms list
sequenzy forms list --json
# Apply partial audience and copy updates
sequenzy forms update form_abc123 --headline "Weekly planning" --button-text "Get the planner"
sequenzy forms update form_abc123 --list list_123 list_456 --clear-tags
# Restyle a form or replace its complete block array
sequenzy forms update form_abc123 --theme-file ./form-theme.json
sequenzy forms update form_abc123 --blocks-file ./form-blocks.json --json
```
Run `forms list --json` before replacing blocks, modify the returned block
array, and send the complete array with `--blocks-json` or `--blocks-file`.
Updates are partial, but replacement blocks must retain exactly one email field
and one submit button. Use `--clear-tags` to remove all saved form tags, and
pass an empty `--redirect-url` value to switch back to the confirmation message.
Blocks are the same payload the API takes. The
[Update Saved Form reference](/api-reference/widgets/update-saved-form)
documents every `form-field` property (field types, `mapsTo` targets, choice
`options`, and hidden field behavior). Validation failures name the offending
property, for example `blocks[3].children[0].options[0].value: ...`. Use a
`group` block with `layout: "stack"`, `"row"`, `"grid"`, or `"overlay"` and a
`children` array to keep related blocks together; groups may nest up to three
levels and responsive rows/grids collapse to one column on small screens.
Overlay requires exactly one direct image. `overlayColor`, `overlayShade`
(0-100), and `overlayPosition` (`top`, `center`, or `bottom`) control the image
treatment. `gap` spaces the foreground children without moving the background
image; a nested image only enables Overlay for its own subgroup.
### Popups
Popups are the on-site overlay capture surface, deployed with one script tag.
Use saved forms instead when the signup box should sit inline in the page.
```bash theme={null}
# List popups with their status, views, and conversion rate
sequenzy popups list
sequenzy popups list --json
sequenzy popups list --include-content --json # blocks for every popup
# Read one popup, including its complete block array
sequenzy popups get pop_abc123 --json
# Create a popup from a starting template and print its embed script
sequenzy popups create --name "Welcome offer" --template discount-offer --list list_123
sequenzy popups create --name "Exit capture" --trigger-json '{"type":"exit-intent"}' --status draft
# Apply partial copy, audience, and behavior updates
sequenzy popups update pop_abc123 --headline "Take 10% off" --button-text "Send my code"
sequenzy popups update pop_abc123 --targeting-json '{"paths":["/pricing"],"device":"desktop"}'
# Publish or unpublish without deleting
sequenzy popups update pop_abc123 --status draft
sequenzy popups update pop_abc123 --status published
# Copy a performer into a draft before trying a new offer
sequenzy popups duplicate pop_abc123 --name "Welcome offer v2"
# Print the embed snippets, then delete when it is retired
sequenzy popups embed pop_abc123
sequenzy popups delete pop_abc123
```
Starting templates are `newsletter-modal`, `discount-offer`, `countdown-launch`,
`minimal-slide-in`, `exit-lead-magnet`, `live-demo`, `launch-modal`,
`paper-digest`, `stark-takeover`, `top-bar`, `announcement-bar`, and
`fullscreen-welcome`.
Unlike saved forms, `--list` is optional: omit it (or pass `--all-lists`) and the
popup captures into every list, matching the dashboard default. The
`--trigger-json`, `--targeting-json`, `--schedule-json`, `--frequency-json`, and
`--visual-json` patches are merged key by key, so setting one key keeps the rest.
Run `popups get --json` before replacing blocks and send the complete array.
Each block kind accepts only its own properties, and anything else is rejected by
name rather than dropped - a `custom-html` block carries its markup in `html`
with a pixel `height`, not `content`. See [Content
blocks](/api-reference/widgets/update-popup#content-blocks) for every kind.
Popup block arrays support the same recursive `group` layout as forms.
The `image` visual style needs an https `imageUrl`. Setting the style without one
is rejected, because the image panel is painted as a background: a missing or
blocked image would otherwise leave an empty column beside your content. Popups
that reach a visitor with an unusable image fall back to the accent rail and stay
full width.
`popups list` omits content blocks by default because each popup adds roughly
1.8k characters; use `popups get --json` for one popup, or
`--include-content` when you really need them all.
`popups update --status draft` stops a popup showing while keeping its view
and conversion counts and leaving the embed script valid; `popups delete` removes
the popup and its counts for good. `popups duplicate` copies a popup into a draft
with its own counts, so the original keeps running while you test a variant.
Listings report the funnel as views, conversions, and conversion rate. A rate
shows as `-` rather than `0.0%` when nothing has been measured yet, so an unseen
popup is not mistaken for one that fails to convert.
### A/B Tests
```bash theme={null}
# List A/B tests, optionally scoped to a sequence
sequenzy ab-tests list
sequenzy ab-tests list --sequence seq_abc123
# Get variants and localization status
sequenzy ab-tests get ab_abc123
# Update campaign or sequence settings
sequenzy ab-tests update ab_abc123 --test-percentage 30 --duration-minutes 120 --winner-criteria open_rate
sequenzy ab-tests update ab_abc123 --test-type content --winner-criteria open_rate --winner-threshold 150 --confirm-live-change
# Get aggregate and per-variant stats, plus whether the leader is
# statistically significant on the winning metric
sequenzy ab-tests stats ab_abc123 --period 30d
# Select a campaign winner and begin remaining-audience delivery,
# or select or change a sequence test's winner
sequenzy ab-tests select-winner ab_abc123 var_b
# Keep testing a sequence test that already has a winner
sequenzy ab-tests resume ab_abc123 --confirm-live-change
# Update a variant (campaign variants in draft only; sequence variants any time)
sequenzy ab-tests update-variant ab_abc123 var_b --subject "Better subject"
sequenzy ab-tests update-variant ab_abc123 var_b --html-file ./variant-b.html
sequenzy ab-tests update-variant ab_abc123 var_b --blocks-file ./variant-b-blocks.json
# Create an A/B test for a draft campaign
sequenzy ab-tests create camp_abc123
sequenzy ab-tests create camp_abc123 --test-percentage 30 --duration-minutes 120 --winner-criteria click_rate
sequenzy ab-tests create camp_abc123 --variants-json '[{"subject":"Alternative subject"}]'
sequenzy ab-tests create --automation-node node_abc123 --test-type content --winner-threshold 100 --variants-json '[{"subject":"Variant B"}]'
# Add a variant to a draft campaign or sequence A/B test
sequenzy ab-tests add-variant ab_abc123 --subject "Alternative subject"
sequenzy ab-tests add-variant ab_abc123 --subject "Alternative subject" --blocks-file ./variant.json
# Remove a variant from a draft campaign or sequence A/B test
sequenzy ab-tests delete-variant ab_abc123 var_b --yes
# Delete a campaign A/B test
sequenzy ab-tests delete ab_abc123 --yes
```
Use `get` first to inspect the effective `settings` object and discover variant
IDs. Campaign settings are editable only in draft. Sequence settings can be
updated later with `--confirm-live-change` when the test is active or has
recorded activity.
`ab-tests select-winner` on a campaign test requires `testing` status and
immediately queues the winning variant for the remaining audience. On a
sequence test it selects or changes the winner, including one picked
automatically; contacts who already got a variant keep it, and everyone who
reaches the step afterwards gets the new winner. Add `--confirm-live-change`
when the sequence is active. Campaign tests need the `campaigns:send`
permission; sequence tests need `sequences:write`.
`ab-tests resume` clears a sequence test's winner so contacts are split across
the variants again, keeping the results so far. The test then waits for
`ab-tests select-winner` instead of picking automatically; turn automatic
selection back on with `ab-tests update --auto-select-winner`. Add
`--confirm-live-change` when the sequence is active.
`ab-tests create` accepts either a campaign ID or `--automation-node`. Campaigns must be in draft or rejected status; an automation node must be an email step and is converted to a dashboard A/B-test node. Add `--confirm-live-change` when converting a node in an active sequence. Control variant A is created automatically from the current email; sequence variants receive independent email templates. Extra variants come from `--variants-json` or `--variants-file` (each entry takes `subject`, optional `previewText`, and optional `blocks`). Campaign tests use audience percentage and duration; sequence tests use `--test-type`, `--winner-threshold`, and `--winner-criteria`. An explicit sequence winner criterion overrides the test-type default.
Variants can only be added or removed while the test is in draft status, variant A is the control and cannot be deleted, and running tests cannot be deleted. When a sequence test's parent sequence is active, `add-variant` and `delete-variant` also require `--confirm-live-change` because they immediately change the live rotation.
#### Editing the copy of a sequence A/B step
Converting a sequence email step to an A/B test moves that step's copy onto the
test's variants. `sequences get` still lists the step, now with its `abTest`
summary and one line per variant, but the subject and blocks shown on the step
itself are control variant A only. With `ab_tests:read`, `sequences get --json`
includes each variant's full `blocks` on `abTest.variants[]`.
`sequences update-node` cannot change variant copy - it edits the step's
label, sender, reply profile, and cc/bcc. Apply content changes per variant,
and repeat a change meant for the whole step on every variant so you do not
change what the test measures. Never rebuild the step as a plain email step
to reach its copy: that destroys the test.
```bash theme={null}
# 1. Read A/B steps including each variant's blocks
sequenzy sequences get seq_abc123 --json | jq '.sequence.emails[] | select(.nodeType == "action_ab_test") | {nodeId, abTest}'
# 2. Apply the change once per variant
sequenzy ab-tests update-variant ab_abc123 var_a --blocks-file ./variant-a.json --confirm-live-change
sequenzy ab-tests update-variant ab_abc123 var_b --blocks-file ./variant-b.json --confirm-live-change
```
The full read-and-update workflow needs `ab_tests:read`, `ab_tests:write`, and
`sequences:write` on the API key. The variant edit itself needs both write
scopes.
### Sequences
```bash theme={null}
# List sequences
sequenzy sequences list
# Get sequence details
sequenzy sequences get seq_abc123
# Send one saved email step to internal reviewers
sequenzy sequences test seq_abc123 --node node_email_2 \
--to reviewer-one@example.com reviewer-two@example.com
# Render one saved email step to email-ready HTML (read-only)
sequenzy sequences render seq_abc123 --node node_email_2 --out preview.html
# Create the same blank trigger-to-completion draft as the dashboard
sequenzy sequences create cancellation-feedback
# Filter the dashboard sequence list
sequenzy sequences list --status active --search cancellation --label Lifecycle --limit 25
# Create an AI-generated sequence with custom spacing and a plain, text-first style
sequenzy sequences create onboarding --trigger contact_added \
--goal "Guide new users to activation" \
--email-count 4 \
--duration-days 10 \
--email-style plain
# Only start once the contact joins a list, instead of for every contact added
sequenzy sequences create newsletter-welcome --trigger contact_added \
--list-scope any_list \
--goal "Welcome new list members"
# Start when the contact joins EITHER list (--tag-names works the same for tags)
sequenzy sequences create welcome --trigger contact_added \
--list-ids list_abc123 list_def456 \
--goal "Welcome new subscribers"
# Create a purchase sequence scoped to one product (event property filters)
sequenzy sequences create deliver-ebook --trigger event_received \
--event-name ecommerce.order_placed \
--property-filters-json '[{"path":"lineItems[].providerProductId","operator":"equals","value":"prod_123"}]' \
--steps-file ./steps.json
# Create a dynamic discount sequence (code generated per subscriber at runtime)
sequenzy sequences create win-back --trigger tag_added \
--tag-name cancelled \
--steps-json '[{"type":"create_discount","discount":{"provider":"shopify","discountType":"percent","percentOff":20,"appliesToAllPlans":true,"maxRedemptions":1,"codePrefix":"WINBACK"}},{"subject":"Come back with {{discount.code}}","html":"
Use {{discount.code}} for 20% off.
"}]'
# Create a sequence that copies purchase-event data onto the subscriber
sequenzy sequences create purchase-profile --trigger event_received \
--event-name saas.purchase \
--steps-json '[{"type":"update_subscriber","nodeType":"action_update_attributes","config":{"customAttributeUpdates":[{"name":"plan","value":"{{event.plan}}","valueType":"text"},{"name":"mrr","value":"{{event.amount}}","valueType":"number"}]}}]'
# Update sequence metadata and existing email steps
sequenzy sequences update seq_abc123 --name "Updated onboarding"
sequenzy sequences update seq_abc123 --steps-file ./sequence-updates.json
sequenzy sequences update seq_abc123 \
--emails-json '[{"nodeId":"node_founder_note","emailPreset":"minimal"}]'
# Repaint one email step's background without moving the account-wide theme
# (partial patch; send "emailTheme": null to go back to the company theme)
sequenzy sequences update seq_abc123 \
--node-updates-json '[{"nodeId":"node_founder_note","expectedUpdatedAt":"2026-08-20T10:00:00.000Z","changes":{"emailTheme":{"colors":{"background":"#ffffff"}}}}]'
sequenzy sequences update seq_abc123 \
--emails-json '[{"nodeId":"node_founder_note","emailTheme":{"colors":{"background":"#ffffff"}}}]'
sequenzy sequences update seq_abc123 \
--subscriber-update-steps-json '[{"nodeId":"node_update","config":{"customAttributeUpdates":[{"name":"active","value":"{{event.active}}","valueType":"boolean"}]}}]'
sequenzy sequences update seq_abc123 \
--stop-condition-json '{"type":"has_tag","value":"customer"}'
# Stop only when a later event matches property filters (event stops only
# count events received after enrollment)
sequenzy sequences update seq_abc123 \
--stop-condition-json '{"type":"event_received","value":"product.credit_consumed","matchConfig":{"mode":"event_property_filter","propertyFilters":[{"path":"quota_used","operator":"greater_than","value":1}]}}'
# Atomically replace the trigger
sequenzy sequences update seq_abc123 --trigger inbound_webhook \
--event-name summit.cancellation_feedback \
--custom-integration-file ./heysummit-webhook.json \
--confirm-live-change
# BCC team inboxes on every email the sequence sends
sequenzy sequences update seq_abc123 --bcc-emails "cs-team@yourcompany.com, founder@yourcompany.com"
sequenzy sequences update seq_abc123 --clear-bcc-emails
# Attach a file to an email step (fetched from the URL at send time; [] removes)
sequenzy sequences update seq_abc123 \
--emails-json '[{"nodeId":"node_email","attachments":[{"filename":"guide.pdf","path":"https://example.com/files/guide.pdf"}]}]'
# Resolve a unique attachment from the event that enrolled the subscriber
sequenzy sequences update seq_abc123 \
--emails-json '[{"nodeId":"node_file","attachments":[{"filename":"file-{{event.order_id}}.pdf","path":"{{event.file_url}}"}]}]'
# Insert new linear steps after an existing node
sequenzy sequences update seq_abc123 \
--insert-steps-json '{"afterNodeId":"node_email","steps":[{"delay":{"days":1},"subject":"Migration check-in","html":"
Need help migrating?
"}]}'
# Insert an if/else branch after an existing node
sequenzy sequences update seq_abc123 \
--branch-file ./branch.json
# Wait for a reply, then continue or time out after five days
sequenzy sequences update seq_abc123 \
--insert-steps-json '{"afterNodeId":"node_email_1","steps":[{"nodeType":"logic_wait_for_event","config":{"label":"Wait for reply","eventName":"email.replied","timeoutDays":5,"timeoutAction":"continue"}}]}'
# Route replies to completion and everyone else to the existing follow-up
sequenzy sequences update seq_abc123 \
--branch-json '{"afterNodeId":"node_email_1","branches":[{"id":"replied","conditionType":"event_received","eventName":"email.replied","activityScope":"this_sequence","targetNodeId":"node_complete"}],"elseTargetNodeId":"node_email_2"}'
# Split abandoned-cart contacts 50/50 between two offers (concurrent A/B test)
sequenzy sequences update seq_abc123 \
--branch-json '{"afterNodeId":"node_cart_wait","splitMode":"random","randomPercentages":[50,50],"branches":[{"id":"discount","label":"10% off","steps":[{"subject":"Here is 10% off","html":"
Complete your order.
"}]},{"id":"bonus","label":"Free shipping","steps":[{"subject":"Free shipping on us","html":"
Complete your order.
"}]}]}' \
--confirm-structural-change
# Enable/disable
sequenzy sequences enable seq_abc123
sequenzy sequences disable seq_abc123
sequenzy sequences pause-enrollments seq_abc123
sequenzy sequences resume-enrollments seq_abc123
```
`sequences list`, `sequences get`, and `sequences update` print the stored
status together with the resolved run state, because `active` alone is
ambiguous: a sequence with paused enrollment still emails its current recipients
but takes nobody new. The four run states you will see are `active (running)`,
`active (new enrollments paused, existing recipients continue)`,
`paused (recipients frozen, nobody new enters)`, and `draft`/`archived`
(`not running`). `enable`, `disable`, `pause-enrollments`, and
`resume-enrollments` print the resulting state on a `State:` line. With `--json`
the same information is available as `effectiveStatus`,
`acceptsNewEnrollments`, `processesExistingEnrollments`, and
`effectiveStatusSummary`.
```bash theme={null}
# Clone a public email gallery sequence into a draft in your brand
sequenzy sequences from-example linear/onboarding --brief "Push people to invite a teammate"
sequenzy sequences from-example linear/onboarding --steps 1,2,4
# Preview its first emails in your brand (or --website acme.com) without saving
sequenzy sequences preview-example linear/onboarding
# Sequence-list actions
sequenzy sequences duplicate seq_abc123 --name "Welcome Series v2"
sequenzy sequences archive seq_abc123
sequenzy sequences unarchive seq_abc123
# Persisted dashboard conversion goals
sequenzy sequences goals create seq_abc123 Reactivated \
--trigger-type event --trigger-event-name account.reactivated \
--attribution-window-hours 168
sequenzy sequences goals create seq_abc123 Tagged \
--trigger-type tag_added --trigger-tag-name vip
sequenzy sequences goals create seq_abc123 Upgraded \
--trigger-event-name plan.upgraded --inactive
sequenzy sequences goals list seq_abc123
# Configure and rotate an inbound-webhook sequence endpoint
sequenzy sequences webhook configure seq_abc123 \
--field-mapping-file ./field-mapping.json \
--sample-payload-file ./sample-payload.json
sequenzy sequences webhook get seq_abc123
sequenzy sequences webhook rotate seq_abc123
# Manually enroll subscribers in a sequence
sequenzy sequences enroll seq_abc123 --email one@example.com two@example.com
sequenzy sequences enroll seq_abc123 --subscriber-ids sub_123 sub_456 --target-node-id node_123
sequenzy sequences enroll seq_abc123 --emails-file ./vips.csv
sequenzy sequences enroll seq_abc123 --emails-json '["one@example.com"]' --target-node-id node_123
sequenzy sequences enroll-audience seq_abc123 --segment-id seg_past_buyers
sequenzy sequences enroll-audience seq_abc123 --all --apply --wait
sequenzy sequences enroll-audience seq_abc123 --list-ids list_abc --apply --schedule-at 2026-11-15T09:00:00Z --keep-enrolling
sequenzy sequences enroll-audience seq_abc123 --segment-id seg_past_buyers --apply --data-json '{"sale_ends_at":"2026-11-29T23:59:00Z","discount_percent":20}'
sequenzy sequences update seq_abc123 --auto-enroll-list-ids list_abc --auto-enroll-starts-at 2026-11-15T09:00:00Z
sequenzy sequences update seq_abc123 --clear-auto-enroll
sequenzy sequences audience-enrollments seq_abc123
sequenzy sequences audience-enrollment seq_abc123 aer_abc123
sequenzy sequences cancel-audience-enrollment seq_abc123 aer_abc123
# List the contacts currently enrolled, and export everyone waiting at one step
sequenzy sequences enrollments seq_abc123
sequenzy sequences enrollments seq_abc123 --status waiting --sort wait_until_asc
sequenzy sequences enrollments seq_abc123 --node-id node_wave_1 --all --csv enrollments.csv
# Find out why enrollments stopped
sequenzy sequences enrollments seq_abc123 --status failed
# Each row prints "entered via ..." - which list, tag, segment, or event
# enrolled the contact - and the last branch decision when one was recorded.
# CSV exports flatten entry source and the last branch summary.
sequenzy sequences enrollments seq_abc123 --all --csv enrollments.csv
# Why one completed token skipped email: graph walk + redacted branch decision
sequenzy sequences enrollment seq_abc123 tok_abc123
# Check whether a stop condition already matches, without waiting out the delay
sequenzy sequences enrollments seq_abc123 --subscriber-id sub_abc123 --stop-condition-match
# Dry-run cancellation for enrollments with matching entry event values
sequenzy sequences cancel-enrollments seq_abc123 --field-path order.id --field-values ord_123,ord_456
# Preview, then drain, every active or waiting enrollment in a sequence
sequenzy sequences cancel-enrollments seq_abc123 --all
sequenzy sequences cancel-enrollments seq_abc123 --all --apply --reason "Lifecycle cutover"
# Cancel a specific batch of enrollments
sequenzy sequences cancel-enrollments seq_abc123 --subscriber-ids sub_123 sub_456 --apply
# Apply cancellation for one subscriber enrollment
sequenzy sequences cancel-enrollments seq_abc123 --subscriber-id sub_abc123 --apply
# Release the next 180 contacts waiting on a delay, without cancelling anyone
sequenzy sequences move-enrollments seq_abc123 --from-node node_delay_2 --limit 180
sequenzy sequences move-enrollments seq_abc123 --from-node node_delay_2 --limit 180 --daily-limit 500 --tag wave-3-released --reason "Wave 3" --apply
# After changing the sending window, pull waiting contacts back to its opening
sequenzy sequences realign-enrollments seq_abc123
sequenzy sequences realign-enrollments seq_abc123 --apply
sequenzy sequences realign-enrollments seq_abc123 --job-id
sequenzy sequences realign-enrollments seq_abc123 --node-ids node_week_14 --apply
```
`sequences enrollments` is the contact-level view behind the counts in `stats --sequence`. It defaults to active and waiting enrollments and prints each contact's email, name, current node, and scheduled resume time. Filter with `--node-id`, `--status`, `--subscriber-id`, or `--email`, sort with `--sort wait_until_asc` to see who resumes next, and use `--all --csv ` to export every match. Node IDs come from `sequences get` or from `enrollmentCounts.byCurrentNode` in `stats --sequence`. CSV exports also include entry trigger type, event property keys, and the last redacted branch decision (selected path, compared field, missing/empty/nonempty/equals\_expected).
`sequences enrollment ` reads one token, including bounded ClickHouse `nodeHistory`. Use it when a completed row lists as entered via unknown or sitting on the completion node: reconstructed branch decisions are redacted (field name plus missing/empty/nonempty/equals\_expected), never the raw compared value. The command prints warnings when node or branch history was truncated. Take `enrollmentId` from `sequences enrollments`.
Use `--status failed` to triage a step that is not delivering. Failed enrollments are terminal - they are not retried - and each row prints a `failed:` line with the reason the worker recorded. The same reason on the same node across several contacts points at that step's content or configuration rather than at the contacts.
The command also prints the sequence's single configured stop condition and any `matchConfig` event-property filters, field comparison, or `entry_audience` resolver. For entry-audience defaults, `value` is null and the enrolling tag or list is resolved per contact. A stop condition is re-checked when an enrollment next runs a step, not when its event arrives, so a contact whose stop event already landed keeps reporting `waiting` until its delay expires. Pass `--stop-condition-match` to inspect the current state: matching enrollments print a `stop condition matches now:` line with the reason. This is a non-atomic snapshot, so a step already past its stop check may still finish. The option caps the page at 100 rows, and a missing annotation means "not determined", not "does not match". See [when a stop condition actually cancels](/concepts/sequences#when-a-stop-condition-actually-cancels).
`sequences move-enrollments` releases a bounded batch of contacts off one step and onto another while keeping their enrollment. Use it to let the next N contacts waiting on a delay continue early instead of cancelling and re-enrolling them, which throws away the enrollment's entry event properties, stop-condition snapshots, and start date. `--from-node` is required; `--to-node` defaults to the source step's only next step. Without `--apply` the command reports what would move. Unlike `cancel-enrollments` it never auto-drains - the bound is the point - so re-run it while `remainingCount` is above zero. `--daily-limit` refuses to release more than that many onto the target step in a rolling 24 hours, and `--tag` marks the released wave with existing tags so `sequences enrollments` can tell it apart later. It also works while new enrollment is paused, because these contacts are already enrolled.
`sequences cancel-enrollments` takes exactly one target: `--all`, `--subscriber-id`, `--subscriber-ids`, or `--field-values`. Use `--all` to fully stop a live sequence, including segment-triggered contacts that share no entry field value - pausing enrollment only blocks new entrants, it does not remove contacts already mid-flight. Without `--apply` the command reports matches and changes nothing. With `--apply` it keeps calling the API until every match is drained, printing running progress, because each request cancels at most 1,000 enrollments.
`sequences realign-enrollments` is the follow-up to changing a live sequence's sending window. Existing waits keep the time their delay produced, so a widened window never reaches contacts already parked on an email-bound delay step and a narrowed one defers them to the next allowed day. Realignment pulls each waiting contact back to the window's opening on the day they were already scheduled for: a wait only ever moves earlier, never onto a different local day, and never before now, and nobody is cancelled or re-enrolled. Sequence windows do not advance SMS, webhooks, branches, or other non-email actions. Without `--apply` it only reports the new times. An applied request runs in the background and prints a `--job-id` command for reading its result. Narrow it with `--node-ids` or `--subscriber-ids`; when the completed result reports more matches, run the complete continuation command it prints so `--apply` and the original targeting filters are retained.
`sequences enroll` accepts subscriber IDs with `--subscriber-ids`, the same email inputs as `lists add-subscribers` (`--email`, `--emails-json`, or `--emails-file`), or both. Each API request is capped at 500 total targets across emails and subscriber IDs. Only active subscribers are enrolled: unknown emails are reported as not found, and inactive, unavailable, or already enrolled subscribers are skipped. Enrollment starts at the first step after the trigger unless you pass `--target-node-id`, and the sequence must be accepting entrants (enabled and not paused for enrollment).
`sequences test` sends only the selected saved email step. Use `sequences get`
to find its `nodeId`, then pass one to ten internal addresses after `--to`.
The JSON response includes one durable `emailSendId` per recipient for delivery
inspection.
`sequences simulate` is the dry run before `sequences enable`. Without a
subscriber it prints who currently matches and whether the sequence is ready.
Nobody is auto-enrolled when you activate; list, tag, and segment matches can
still be enrolled by hand.
Pass `--subscriber` or `--email` to walk one stored contact through the
graph. Use `--limit` to return between 1 and 25 currently matching contacts
(default 10). Nothing is sent.
```bash theme={null}
sequenzy sequences simulate seq_abc123
sequenzy sequences simulate seq_abc123 --email maya@example.com --limit 25
```
For `sequences update`, use `sequences get` first to find `nodeId` and `emailId` values. Metadata flags include `--description`, repeatable `--label` (or `--clear-labels` to remove every label), `--user-cancellable`/`--not-user-cancellable`, and sequence or per-step sender/reply identity. Trigger replacement uses `--trigger` plus its typed trigger flags and is atomic; on an active sequence it requires `--confirm-live-change`. `--steps-file` and `--emails-file` edit existing email steps by `nodeId`, `emailId`, or array order; items can set transactional mode and CC/BCC recipients. An email item can set `emailPreset` to `minimal` or `branded` for native Sequenzy blocks, including emails that contain supported custom HTML blocks, using the same format transformation as the dashboard. An email stored entirely as one standalone raw HTML block does not support `emailPreset`, and `emailPreset` cannot be combined with `html` or `htmlContent`. An email item can also set `emailTheme` to restyle that step alone: it is a partial patch over the step's current override (then the company theme), and `"emailTheme": null` drops the override so the step inherits the company theme again - the workspace default is never rewritten. `sequences get` prints each step's stored override under `Theme`. `--insert-steps-file` inserts new linear steps with an optional `afterNodeId`, including delay, tag/list, outbound webhook, AI, condition, discount, subscriber-update, SMS, and `logic_wait_for_event` nodes with typed config. Tag, list, wait, condition, webhook, and AI steps put their node fields in `config`: `action_add_tag`/`action_remove_tag` take `tagName` (or `tagId` when you have the real tag ID), `action_add_to_list`/`action_remove_from_list` take `listId`, `logic_wait_for_event` takes `eventName` with `timeoutDays`/`timeoutAction`, `logic_condition` takes `conditionType` plus its resource field, and `action_webhook` takes an HTTPS `url` with optional `method` (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`), `headers`, a JSON `body` template (valid JSON as written, merge tags inside quoted values), `resultKey` (saves the response for later `{{webhooks.KEY.data.field}}` merge tags), and `onError` (`continue`/`exit`/`fail`). `action_ai` takes a merge-tag `prompt`, a required `resultKey`, `outputFields` (`[{ key, description, maxLength, fallback }]`, with `fallback` used when generation fails), optional `includeTags`/`includeEventProperties`/`includeRecentEvents` (with `recentEventLimit`, 1-50, default 10)/`includeAttributes` context selectors, and `onError` (default `continue`); later steps read the output with `{{ai.KEY.field}}` merge tags. SMS, discount, and delay steps keep using their step-level fields. Use `--branch-file` for a new if/else node; each path can provide new `steps`, an existing `targetNodeId`, or both, while `elseTargetNodeId` wires the fallback. A path's `steps` array is one linear chain, so it cannot contain another branch: end the path with the step the nested branch should follow, then insert the nested branch with a second command whose `afterNodeId` is that path's last node - its paths reconnect to the shared steps that already followed it. The command prints the new branch node and every path's node IDs (`addedBranchPathNodeIds` with `--json`), so the second command needs no extra `sequences get`. Setting `"splitMode":"random"` with `"randomPercentages"` turns the same node into a weighted A/B split that routes by percentage instead of by condition; its paths omit `conditionType` and condition-specific fields, and it has no else path. Do not combine branch insertion, linear insertion, or a graph edit in the same command.
When the target sequence is active, structural edits such as `--insert-steps-*`, `--branch-*`, or `--graph-edit-*` require `--confirm-structural-change` after you confirm the live-flow impact. Content-only `--steps-*` and `--emails-*` edits do not require that flag.
Deleting a step with `--graph-edit-*` immediately moves parked recipients to
its unique surviving successor, or completes them when no successor remains.
The command reports both affected counts in human output and under
`sequence.migratedRecipientCount` / `sequence.completedRecipientCount` with
`--json`.
### Email blocks
Anything that writes email content from a file - `components create --blocks-file`,
`campaigns create --blocks-file`, the REST API, the MCP tools - takes a blocks
array whose members come from a large union. `sequenzy blocks` is the field
reference for them.
For a custom YouTube cover, set `thumbnailUrl` on the video block. CLI block
files are forwarded unchanged; omit the field in a replacement write to restore
YouTube's own still while keeping `videoUrl` as the click destination.
```json theme={null}
{
"type": "video",
"videoUrl": "https://www.youtube.com/watch?v=...",
"thumbnailUrl": "https://cdn.example.com/cover.jpg",
"alt": "Watch the product tour"
}
```
```bash theme={null}
# List every block type with its required fields
sequenzy blocks
# Hide the structural types the editor manages for you
sequenzy blocks --creatable-only
# Full field reference for one type, with an example
sequenzy blocks list
sequenzy blocks steps --example
sequenzy blocks hero --json
```
Each entry names the required and optional fields, the allowed values of every
enum field, and the shape of nested item arrays and nested objects - so
`sequenzy blocks repeat` prints the `mode`, `strategy`, `sort`, and `filters`
that a `productSource` accepts, not just `productSource: object`. The one shape
worth knowing without looking it up: lists are their own block type, not a text
variant.
```json theme={null}
{ "type": "list", "variant": "numbered", "items": [{ "content": "First" }] }
{ "type": "steps", "variant": "numbered", "items": [{ "title": "Step one", "description": "What happens" }, { "title": "Step two", "description": "What happens next" }] }
```
A `text` block accepts only `variant: "paragraph" | "lead" | "html"` and never
accepts `items`. Use `list` for a plain numbered or bulleted list and `steps`
for a visual numbered walkthrough. List items carry `content`; steps items carry
`title` and an optional `description`.
Every block also takes a `conditions` array that hides it unless the contact
matches, and `conditional-group` wraps an if/else pair around the same filters.
Their `field` and `operator` entries are two flat lists that the validator
narrows against each other, so reading them alone suggests `tag is vip` is legal
when only `tag contains vip` is. `sequenzy blocks conditional-group` prints the
real table: the operators each field accepts, how that field's `value` string is
shaped, and what a `render` needs before it can evaluate the condition instead
of rendering it as false. Any other type takes `--conditions`, since the table
is longer than the block reference itself; `sequenzy blocks --json` carries it
for every type at once.
```bash theme={null}
sequenzy blocks conditional-group
sequenzy blocks text --conditions
```
This is static reference data derived from the same schemas that validate a
write, so it cannot drift from what the API accepts. When a block write is
rejected, the error already names the block type it was validated against, that
type's fields, and any field the type does not accept.
### Email components
Components are saved block groups you can reuse across emails. The component
pinned to the `footer` slot is the footer every new sequence, campaign, and
AI-generated email is built with, so editing it changes the footer everywhere at
once - editing one email's blocks only changes that email.
```bash theme={null}
# Read the current default footer, edit it, write it back
sequenzy components footer get
sequenzy components footer get --json > footer.json
sequenzy components footer set --blocks-file ./footer.json --yes
# List components, or only the ones pinned as company defaults
sequenzy components list
sequenzy components list --defaults-only --json
# Manage reusable saved sections
sequenzy components create "Promo banner" --blocks-file ./banner.json
sequenzy components get cmp_abc123 --json
sequenzy components update cmp_abc123 --blocks-file ./banner-v2.json
sequenzy components delete cmp_abc123 --yes
```
`components footer set` replaces the whole footer, so read the current one with
`components footer get --json` first when you only mean to change part of it. A
default footer always keeps its unsubscribe link enabled; transactional sends
hide it at render time. Without application options, existing emails keep their
saved footer. Use the preview and apply workflow below to update selected existing
emails. `components update` only bumps the component version.
### Templates
```bash theme={null}
# List templates (newest first, 50 per page)
sequenzy templates list
sequenzy templates list --company comp_abc123
sequenzy templates list --json
# Page through a long template history
sequenzy templates list --limit 100 --json
sequenzy templates list --limit 100 --offset 100 --json
# Advance --offset by pagination.count while pagination.hasMore is true.
# Only reusable master designs (emails marked as templates)
sequenzy templates list --templates-only
# Get template
sequenzy templates get tmpl_abc123
sequenzy templates get tmpl_abc123 --company comp_abc123 --json
# Render a template to email-ready HTML
sequenzy templates render tmpl_abc123 --out preview.html
# Create or update with HTML
sequenzy templates create welcome --subject "Welcome" --html-file ./welcome.html
sequenzy templates update tmpl_abc123 --subject "Updated" --html-file ./welcome-v2.html
sequenzy templates update tmpl_abc123 --preview-text "Everything you need to get started"
# Create or update with Sequenzy blocks
sequenzy templates create welcome --subject "Welcome" --blocks-file ./welcome-blocks.json
sequenzy templates update tmpl_abc123 --blocks-file ./welcome-v2-blocks.json
# Mark a master design as a reusable template (or remove the mark)
sequenzy templates create lookbook --subject "The lookbook" --blocks-file ./lookbook.json --template
sequenzy templates update tmpl_abc123 --template
sequenzy templates update tmpl_abc123 --no-template
# Store a caller-supplied localized variant
sequenzy templates localizations set tmpl_abc123 es --subject "Bienvenido" --html-file ./welcome-es.html
# Store it and keep automatic translation on save from replacing it
sequenzy templates localizations set tmpl_abc123 es --subject "Bienvenido" --html-file ./welcome-es.html --keep-edits
# Queue AI translation for selected locales, or omit --locale to sync all
sequenzy templates localizations sync tmpl_abc123 --locale es fr
sequenzy templates localizations sync tmpl_abc123
# Keep localizations someone edited instead of retranslating them
sequenzy templates localizations sync tmpl_abc123 --skip-edited
# Remix a public email gallery example into a template in your brand
sequenzy templates from-example https://sequenzy.com/email-examples/brands/linear/emails/welcome-to-linear
sequenzy templates from-example linear/welcome-to-linear --brief "Mention our 14-day trial"
# An email shown inside its sequence: copy the URL with its #email- hash
sequenzy templates from-example "https://sequenzy.com/email-examples/brands/linear/sequences/onboarding#email-welcome-to-linear"
# Preview the rewrite without saving it; --json includes the HTML
sequenzy templates preview-example linear/welcome-to-linear --website acme.com --json
# Create or revoke a public "view in browser" link
sequenzy templates share tmpl_abc123
sequenzy templates share order-confirmation
sequenzy templates unshare tmpl_abc123
```
Templates are the company's saved email bodies: standalone templates plus the
bodies behind campaigns and transactional emails, so emails designed in the
dashboard appear in `templates list` too. A campaign's `emailId` (from
`campaigns get`) resolves with `templates get`, and bodies are kept when their
campaign or transactional email is deleted. Because every campaign body counts,
the list grows with your sending history - use `--limit` and `--offset` to read
past the first page.
`--template` marks an email as a reusable master design. Master designs are
offered first when a dashboard sequence step or campaign starts from an existing email,
and starting from one always creates an independent copy, so editing the copy
never changes the master. `templates list --templates-only` returns just those,
and every list, get, create, and update result prints `Reusable template`
(`isTemplate` with `--json`).
Creating a standalone copy of a saved design and AI rewriting within its layout are currently
dashboard-only; there is no CLI source-design copy or fill command. This is a
[documented interface exception](/concepts/email-templates#availability-across-interfaces).
Use `templates create` with finished HTML or blocks for a new email body;
localized variants must be supplied separately.
`templates from-example` and `sequences from-example` start from a public
[email gallery](https://sequenzy.com/email-examples) example instead: pass the
gallery URL or `brand/slug`, plus an optional `--brief`. The template keeps the
example's exact layout, with the text rewritten and your logo, color and links swapped in. The sequence copies
the example's trigger and timing (up to 12 emails; `--steps 1,2,4` picks which of
the example's emails to keep) as a draft, and AI writes its emails in the
background. See [Create Template from
Example](/api-reference/templates/create-from-example) and [Create Sequence from
Example](/api-reference/sequences/create-from-example).
### Gallery references
`references` lists real emails from the
[email gallery](https://sequenzy.com/email-examples) to model a campaign,
sequence or transactional email on. By default they come from the gallery brands
most like you: the brands whose businesses (what each sells, and to whom) are
closest in meaning to your company description. `--kind` defaults to `campaign`.
```bash theme={null}
sequenzy references --kind transactional --subtype password_reset
sequenzy references --kind sequence --json
sequenzy references --kind campaign --all --search "black friday"
```
Each URL works with `templates from-example` or `sequences from-example`. Each
email shows its main button, length, voice and why it works when it has been
analyzed (`--json` returns the full `analysis`). Add `--all` to browse the whole
gallery for that kind of email, and `--search` to match by meaning. See [List Email References](/api-reference/references/list).
Missing a competitor? `brand-requests` asks for a brand to be added to the
gallery, and shows where each request stands: requested, collecting emails, in
the gallery (with its URL) or not added. Asking again for the same website keeps
one request. See [Request a Brand](/api-reference/references/brand-requests-create).
```bash theme={null}
sequenzy brand-requests add competitor.com --note "Their onboarding emails"
sequenzy brand-requests
sequenzy brand-requests remove req_3jd9
```
`watchlist` follows competitors' emails, like the dashboard's Competitors page.
`watchlist add` watches a brand: one already in the gallery straight away, any
other by requesting it (it also shows in `brand-requests`). `watchlist emails`
lists what the brands you watch sent, newest first; filter with `--brand` and
`--since`, and page with `--cursor`. Emails arrive a few hours after they're
sent. See [Competitor Watchlist](/concepts/competitor-watchlist).
```bash theme={null}
sequenzy watchlist add linear.app
sequenzy watchlist
sequenzy watchlist emails --since 2026-10-01 --limit 10
sequenzy watchlist remove req_8fk2
```
`templates list` includes localization sync status by locale. `templates get`
includes the full localized variants and the effective company localization
config. `templates localizations set` requires one enabled non-primary locale,
a subject, and exactly one HTML or blocks source. Add `--keep-edits` to protect
it from automatic on-save translation like a dashboard edit. `templates localizations
sync` works even when automatic on-save localization is disabled; it
retranslates edited localizations unless you pass `--skip-edited`, which
prints the kept locales. `templates list` and `templates get` mark edited
localizations.
`templates share` creates the same kind of public "view in browser" link as
`campaigns share`, but for an individual email - a transactional email (by ID
or slug), a sequence email (by the step's `emailId` from `sequences get`), or
a standalone template. The hosted page renders an anonymized copy with a
sample contact and no tracking. Rerunning the command returns the same URL
until you revoke it with `templates unshare`, after which the old URL returns
404 and sharing again mints a different one. `templates get` prints the active
link on the `Share URL` line and returns it as `shareUrl` with `--json`.
When you pass blocks JSON, each block can include a `conditions` array for recipient-specific content. Use `field: "variable"` to branch on a value passed in the send's `variables` or an automation `event` payload:
```json theme={null}
[
{
"id": "intro",
"type": "text",
"content": "
",
"variant": "paragraph",
"conditions": [
{ "id": "c1", "field": "variable", "operator": "is", "value": "plan:pro" }
]
}
]
```
In a `variable` condition, the text before the colon in `value` is a merge-tag path (without `{{ }}`), including nested paths like `order.total` or `event.plan`, and the text after it is the comparison value. `field` can also be `attribute` (same `name:value` form) or `email` / `firstName` / `lastName`. Operators: `is`, `is_not`, `contains`, `not_contains`, `gt`, `gte`, `lt`, `lte`, `is_empty`, `is_not_empty`. For if/else, add a `conditional-group` block with `ifBranch.children` and `elseBranch.children`.
### Companies
```bash theme={null}
sequenzy companies list
sequenzy companies get comp_abc123
sequenzy companies get comp_abc123 --json
sequenzy urls --company comp_abc123 --settings-tab billing
sequenzy companies update comp_abc123 --primary-color "#0ea5e9"
sequenzy companies update comp_abc123 --company-context-file ./product.md --tone-voice "clear, direct, warm"
sequenzy companies update comp_abc123 --email-theme-json '{"colors":{"mutedText":"#4b5563"},"typography":{"baseFontSize":16}}'
# Sending identity defaults
sequenzy companies update comp_abc123 --from-email hello@mail.example.com --from-name "Example"
sequenzy companies update comp_abc123 --from-name "Example"
sequenzy companies update comp_abc123 --sender-profile-id sender_abc123 --from-name "Example News"
# Default lists new contacts join when nothing targets them
sequenzy companies update comp_abc123 --default-list list_abc --default-list list_def
sequenzy companies update comp_abc123 --default-lists-none
sequenzy companies update comp_abc123 --default-lists-all
```
`companies get` includes product info, brand colors, AI writing context, the account-wide From and Reply-To defaults, the effective email localization settings, and the read-only `emailBranding` entitlement for that company. Human output says whether the "Sent with Sequenzy" badge is visible and whether an upgrade or renewal removes it; `--json` includes the tier, subscription status, reason, and canonical subscription URL. The badge is decided when each email renders, so upgrading also removes it from future sends by existing live sequences. `urls --settings-tab billing` is a compatibility shortcut to the real **Account -> Subscription** page, not a Settings tab. `companies update` edits the product info that AI uses for generated emails, including `primaryColor`, `companyContext`, `toneVoice`, value props, testimonials, and related brand fields. `--email-design-prompt` / `--email-design-prompt-file` set the brand design prompt - the art direction AI uses when it designs emails (layout, density, imagery, CTA prominence), separate from `--tone-voice`, which steers the copy; when it is empty, the next email generation prefills it with the direction derived from the brand, and passing an empty string clears it so the next generation writes a fresh one. `--email-theme-json` / `--email-theme-file` edit the company's default email theme (`presetId`, `colors`, `typography`, `layout`); updates are partial, and passing `null` resets the theme to the platform default.
`--from-email` / `--reply-to` set the account-wide sending defaults, creating the profile when one does not exist yet (a From address needs a verified sending domain). `--from-name` and `--reply-to-name` on their own rename the display name of the current default profile without changing its address, so campaigns and sequences pinned to that profile pick up the new name.
One address can carry several display names, so pass `--sender-profile-id` (or `--reply-profile-id`) to select exactly which profile becomes the default and which one `--from-name` renames. Run `sequenzy sender-profiles list` to see the IDs. Renaming with `--from-email` on an address that has several display names is rejected, since there is no way to tell which identity you meant.
The default-list flags set which lists a new contact joins when nothing targets it explicitly - forms, API writes, events, tag actions, imports, and live ingestion from integrations that have no list targeting of their own (Dodo Payments, PostHog, Polar, Paddle, WooCommerce). Payment-provider backfills also follow this setting; PostHog history imports are an exception and create contacts without list memberships. `--default-lists-all` means every current and future list, `--default-lists-none` means no list at all, and `--default-list ` (repeatable) means exactly those lists; the three are mutually exclusive, and `companies get` reports the current value. Unknown or foreign list IDs are rejected rather than skipped. An ordinary profile or attribute upsert for an active contact leaves their memberships alone unless it names lists explicitly. Capture submissions and explicit lifecycle reactivations are exceptions: they can reapply target or default lists so a returning contact actually resubscribes. The change applies only to later writes, so a list already filled by an integration backfill still needs `sequenzy lists remove-subscribers`. The same setting lives in the dashboard under **Contacts -> Lists -> Default Lists**.
`companies update` needs a key with `company_profile:manage` for the profile, branding, compliance, and AI-context fields. The sending-identity flags above (`--from-email`, `--from-name`, `--reply-to`, `--reply-to-name`, `--sender-profile-id`, `--reply-profile-id`, and the reply-tracking flags) and the default-list flags additionally need `companies:manage`, and a request that mixes them is rejected as a whole rather than partially applied.
### Segments
```bash theme={null}
# List saved segments
sequenzy segments list
# Preview how many subscribers are in a segment
sequenzy segments count seg_abc123
# Create a Stripe-product segment
sequenzy segments create --name "Bought Pro" --stripe-product prod_pro
# Create a threshold-based Stripe-product segment
sequenzy segments create --name "3+ Pro Payments" \
--stripe-product prod_pro \
--purchase-operator at-least \
--payments 3
# Create a purchased-product segment (Shopify, WooCommerce, or Commerce API orders)
sequenzy segments create --name "Starter Kit Buyers" --commerce-product api:prod-starter-kit
# Create a repeat-buyer segment by order count
sequenzy segments create --name "Repeat Kit Buyers" \
--commerce-product api:prod-starter-kit \
--purchase-operator at-least \
--orders 2
# Create a segment that matches any filter instead of all filters
sequenzy segments create --name "VIP Or Gmail" \
--match any \
--filter-json '[{"field":"tag","operator":"contains","value":"vip"},{"field":"email","operator":"contains","value":"@gmail.com"}]'
# Create a segment from raw filter JSON
sequenzy segments create --name "Custom Filter" \
--filter-json '[{"field":"stripeProduct","operator":"is","value":"prod_pro"}]'
# Combine campaign-specific engagement filters (bounced A but not B)
sequenzy segments create --name "Bounced A Not B" \
--filter-json '[{"field":"emailBounced","operator":"is","value":"campaign:cmp_abc"},{"field":"emailBounced","operator":"is_not","value":"campaign:cmp_xyz"}]'
# Create a nested segment with an event filter
sequenzy segments create --name "Active Non Buyers" \
--filter-json '{"root":{"kind":"group","joinOperator":"and","children":[{"kind":"filter","field":"attribute","operator":"lte","value":"last_login_days_ago:90"},{"kind":"group","joinOperator":"or","children":[{"kind":"filter","field":"event","operator":"is_not","value":"saas.purchase:30d"},{"kind":"filter","field":"segment","operator":"is_not","value":"seg_paying_customers"}]}]}}'
# Update a segment's name, filters, or join operator
sequenzy segments update seg_abc123 --name "Churn Risk"
sequenzy segments update seg_abc123 --filters-json '[{"field":"tag","operator":"contains","value":"vip"}]'
sequenzy segments update seg_abc123 --join-operator or
# Delete a segment
sequenzy segments delete seg_abc123 --yes
```
For Stripe product segments, use the Stripe product ID, not the product name. The CLI maps:
* `bought` -> `field: "stripeProduct", operator: "is", value: "prod_xxx"`
* `didnt-buy` -> `field: "stripeProduct", operator: "is_not", value: "prod_xxx"`
* `at-least` -> `field: "stripeProduct", operator: "at_least", value: "prod_xxx:3"`
* `less-than` -> `field: "stripeProduct", operator: "less_than_count", value: "prod_xxx:3"`
For purchased-product segments (`--commerce-product`), use `provider:productId` where the provider is `shopify`, `woocommerce`, or `api` and the product ID is the one your orders carry in their line items. Product IDs are provider-scoped (Shopify product 42 and WooCommerce product 42 are different products); a bare ID without a provider prefix matches the ID on any provider. The CLI maps:
* `bought` -> `field: "commerceProduct", operator: "is", value: "shopify:42"`
* `didnt-buy` -> `field: "commerceProduct", operator: "is_not", value: "shopify:42"`
* `at-least` -> `field: "commerceProduct", operator: "at_least", value: "shopify:42:2"` (use `--orders`)
* `less-than` -> `field: "commerceProduct", operator: "less_than_count", value: "shopify:42:2"` (use `--orders`)
For purchased-collection segments (`--commerce-collection`), pass a collection ID or handle (for example `skincare`). The filter matches anyone whose orders contain any product currently in that collection, so it is the practical way to segment a large catalog without listing products one by one. You can prefix a provider (`shopify:skincare`) to scope the lookup. The CLI maps:
* `bought` -> `field: "commerceCollection", operator: "is", value: "skincare"`
* `didnt-buy` -> `field: "commerceCollection", operator: "is_not", value: "skincare"`
* `at-least` -> `field: "commerceCollection", operator: "at_least", value: "skincare:2"` (use `--orders`)
* `less-than` -> `field: "commerceCollection", operator: "less_than_count", value: "skincare:2"` (use `--orders`)
Collection membership comes from your synced catalog as it stands right now, not as it stood when the order was placed - moving a product out of a collection removes its past buyers from the segment. Shopify collections and WooCommerce categories are synced automatically; products pushed through the Commerce API do not carry collections yet, so this filter does not match them.
For trial timing, pass `--filter-json` directly:
* `{"field":"stripeTrialProduct","operator":"is","value":"prod_xxx:is_canceled"}`
* `{"field":"stripeTrialProduct","operator":"gte","value":"prod_xxx:start_at:7 days ago"}`
* `{"field":"stripeTrialProduct","operator":"is","value":"prod_xxx:end_at:2026-05-26"}`
Engagement filters (`emailSent`, `emailDelivered`, `emailOpened`, `emailClicked`, `emailBounced`, `emailComplained`) accept a rolling time window (`7d`, `30d`, `90d`, `180d`, `all`), a specific sent campaign (`campaign:`), an email-type scope (`marketing:` for marketing-policy campaign, automation, and Send API traffic; `transactional:` for transactional-policy sends; with `is`, `is_not`, or the bounce-subtype operators), or - with the `at_least` / `less_than_count` operators - a count with a time window (`count:timeRange`, like `10:30d` or `10:all`). For example, `{"field":"emailClicked","operator":"at_least","value":"10:all"}` matches subscribers who clicked 10 or more times ever, and `{"field":"emailSent","operator":"is_not","value":"marketing:7d"}` matches subscribers with no marketing email in the last 7 days. Policy scopes use the policy snapshot captured with the send; ambiguous older automation and Send API events without a snapshot are not assigned to either scope. Combine two campaign-specific filters with the default `--match all` to express rules like "bounced campaign A but not campaign B". Look up campaign IDs with `sequenzy campaigns list`.
For a single engagement rule, dedicated flags avoid writing JSON:
```bash theme={null}
sequenzy segments create --name "Marketing openers" \
--engagement emailOpened \
--engagement-scope marketing \
--engagement-range all
```
`--filter-json` accepts the same filter shape used by the API and MCP. Pass a JSON array for legacy flat filters, or an object with a nested `root` group for v2 nested logic. If you omit filter or group `id` values, the CLI fills them in automatically.
Event filters use `field: "event"` with values like `saas.purchase:30d`, `saas.purchase:all`, or `saas.purchase:5:30d` for count-based operators. Segment filters use `field: "segment"` with the saved segment ID as the value.
By default, `segments create` matches **all** filters with `AND` logic. Pass `--match any` to save the segment with `OR` logic instead.
`segments count` returns the number of **active subscribers** who currently match the segment. In the dashboard you may also see a broader matched-contact count, but campaigns only send to active subscribers.
`segments update` accepts the same filter shapes as `segments create` via `--filters-json` or `--filters-file` (a flat array or an object with a nested `root` group). Provided filters replace the segment's existing rules, and missing filter IDs are filled in automatically. `lists import` is an alias for `lists add-subscribers`.
### Audience Syncs
Push segments to Meta custom audiences for Facebook and Instagram retargeting. Requires the Meta Ads integration to be connected in the dashboard (Settings → Integrations).
```bash theme={null}
# List audience syncs with schedule and status
sequenzy audience-syncs list
# List the connected Meta ad accounts
sequenzy audience-syncs ad-accounts
# Create a sync from a ready-made segment template
sequenzy audience-syncs create --template recent-buyers \
--ad-account act_1234567890 \
--name "Sequenzy - Recent buyers"
# Create a sync from an existing segment, refreshed hourly
sequenzy audience-syncs create --segment seg_abc123 \
--ad-account act_1234567890 \
--name "Sequenzy - VIPs" \
--frequency hourly
# Pause, resume, or change the schedule
sequenzy audience-syncs update sync_abc123 --active false
sequenzy audience-syncs update sync_abc123 --frequency weekly
# Trigger an immediate run
sequenzy audience-syncs sync sync_abc123
# Remove a sync (the Meta audience itself is kept)
sequenzy audience-syncs delete sync_abc123
```
`--template` accepts ready-made segment IDs like `zero-ltv`, `no-purchase-1y`, `recent-buyers`, `high-spenders-ecom`, `non-buyers`, and `engaged`; the segment is created automatically on first use. Audiences are add-only: subscribers who leave the segment stay in the Meta audience.
### Warehouse Sync
Import contacts and events from Snowflake, BigQuery, Redshift or Postgres on a schedule. Each run sends only rows that changed. See [Warehouse Sync](../integrations/warehouse-sync).
```bash theme={null}
# Connect a warehouse (a test query runs before saving)
sequenzy warehouse connections create --provider snowflake --name "Snowflake" \
--account myorg-myaccount --username SEQUENZY_READER \
--warehouse ANALYTICS_WH --database PROD --private-key-file sequenzy_key.p8
sequenzy warehouse connections create --provider bigquery --name "BigQuery" \
--project-id acme-analytics --service-account-file key.json
SEQUENZY_WAREHOUSE_PASSWORD=... sequenzy warehouse connections create \
--provider postgres --name "Analytics" --host db.example.com \
--database analytics --username sequenzy_reader
# See a query's columns before mapping them
sequenzy warehouse connections preview conn_abc123 --query "select email, plan, updated_at from customers"
# Sync contacts incrementally on updated_at
sequenzy warehouse syncs create --connection conn_abc123 --name Customers \
--query-file customers.sql --cursor-column updated_at --frequency hourly \
--mapping-json '{"emailColumn":"email","attributes":[{"column":"plan","attribute":"plan"}]}'
# Record order events on existing contacts
sequenzy warehouse syncs create --connection conn_abc123 --name Orders --kind events \
--query-file orders.sql \
--mapping-json '{"emailColumn":"email","eventName":"order_placed","eventIdColumn":"order_id","occurredAtColumn":"placed_at"}'
# Run now, resend everything, follow progress, pause
sequenzy warehouse syncs run sync_abc123
sequenzy warehouse syncs run sync_abc123 --full-resync
sequenzy warehouse syncs runs sync_abc123
sequenzy warehouse syncs update sync_abc123 --enabled false
```
Mappings and the cursor column are checked against the query's columns when you create or change a sync. Pass the Redshift or Postgres password with `SEQUENZY_WAREHOUSE_PASSWORD` rather than on the command line. `connections update` reads that variable only with `--password-from-env`, so renaming a connection never rotates its password. `--trigger-automations` and `--opt-in-mode double_opt_in` need an API key with `automations:trigger`.
### Data Exports
Stream email events, SMS events, custom events and daily subscriber snapshots to your own S3, Google Cloud Storage or S3-compatible bucket as gzipped JSON lines. See [Data Exports](../integrations/data-exports) for the file layout and warehouse loading.
```bash theme={null}
# List exports with status and per-dataset progress
sequenzy data-exports list
# Add an S3 bucket (the secret is read from the environment)
SEQUENZY_DATA_EXPORT_SECRET=... sequenzy data-exports create \
--name "Analytics lake" \
--bucket acme-exports --region us-east-1 --prefix sequenzy \
--access-key-id AKIA... \
--datasets email_events,custom_events,subscribers \
--frequency every_15_minutes
# Add a GCS bucket with an HMAC key and include the last 7 days
SEQUENZY_DATA_EXPORT_SECRET=... sequenzy data-exports create \
--name "BigQuery feed" --provider gcs --bucket acme-gcs \
--access-key-id GOOG1... --backfill-days 7
# Pause, resume, change datasets or rotate credentials
sequenzy data-exports update exp_abc123 --enabled false
sequenzy data-exports update exp_abc123 --datasets email_events,sms_events
# Check access, export now and inspect recent runs
sequenzy data-exports test exp_abc123
sequenzy data-exports run exp_abc123
sequenzy data-exports runs exp_abc123 --limit 5
# Stop exporting (files already written are kept)
sequenzy data-exports delete exp_abc123 --yes
```
`create` writes a test file before saving and fails with the reason when the bucket, region or key is wrong. Pass the secret with `--secret-access-key` or `SEQUENZY_DATA_EXPORT_SECRET`; it is never returned. `--backfill-days` accepts 0 to 30. Datasets added later start from the time you add them.
### Accounts
Accounts are your customers' organizations. They are unrelated to `sequenzy account`, which shows your own Sequenzy login and workspaces. Your first `accounts upsert` turns Accounts on for the workspace; it fails with a 409 error if your contacts already use a custom attribute named `account` or `account.*`. See [Accounts](./accounts#turning-accounts-on).
```bash theme={null}
# List accounts (bare `sequenzy accounts` also lists)
sequenzy accounts list
sequenzy accounts list --search acme --sort memberCount --order desc
# Show an account and its members
sequenzy accounts get org_123
# Create or update an account by your own organization id
sequenzy accounts upsert org_123 --name Acme --domain acme.com --attr plan=pro --attr seats=5
sequenzy accounts upsert org_123 --attr legacyPlan= # clear a key
sequenzy accounts upsert org_123 --attr plan=team --replace-attributes
# Members and roles (owner, admin, member)
sequenzy accounts members org_123
sequenzy accounts add-member org_123 --email jane@acme.com --role owner
sequenzy accounts remove-member org_123 --email bob@acme.com
# Account events: recorded on the account and delivered to members
sequenzy accounts event org_123 trial_ending --property daysLeft=3
sequenzy accounts event org_123 seat_limit_reached --recipients admins
sequenzy accounts event org_123 plan_changed --occurred-at 2026-09-01T10:00:00Z --event-id plan_change_123
sequenzy accounts events org_123 --limit 20
# Delete the account (contacts are kept)
sequenzy accounts delete org_123 --yes
# Suggest accounts from work email domains your contacts share, then accept the ones you confirm
sequenzy accounts suggestions
sequenzy accounts suggestions --min-contacts 5 --limit 50
sequenzy accounts accept-suggestions acme.com globex.io
# Create accounts from the organization ID your app already sends: find it, preview, create
sequenzy accounts detect-org-ids
sequenzy accounts preview-org-id organization_id
sequenzy accounts create-from-org-id organization_id --wait
sequenzy accounts org-id-job backfill-accounts:comp_123:organization_id
```
Accounts are the companies, workspaces or teams your contacts belong to. Account attributes fan out to every member as `account.` for segments and `{{account.}}` merge tags. `--attr` values are coerced: numbers and `true`/`false` become numbers and booleans, everything else stays a string. Account events reach `owners` by default (falling back to admins when there is no owner), or `admins`, `all` or `none`; each recipient gets a normal contact event with `event.account.*` properties, so sequences and sync rules run per person. `accounts suggestions` is read-only, shows a few sample emails per domain, and skips personal and disposable providers, your sending domains, domains where a contact already belongs to an account and domains an account already uses. `accounts accept-suggestions` takes up to 25 domains, creates one account per domain (external ID and domain set to the domain) with the domain's unassigned contacts as members, or adds them to the one account already using that domain. A domain already partly grouped under another account is skipped. It is safe to retry, and no sync rules or segment-entered sequences run. `accounts detect-org-ids` finds the organization ID your app already sends (an event property or contact attribute such as `workspaceId`). `accounts preview-org-id ` shows the biggest accounts it would create, each named from `--name-key` or the work email domain its contacts share, with sample emails. `accounts create-from-org-id ` shows that preview, asks for confirmation (skip with `--yes`) and starts a background run; add `--wait` to wait for it or check later with `accounts org-id-job `. Existing names, domains and roles are kept, nothing is sent, and reruns are safe. See [Accounts](/concepts/accounts).
### Tags
```bash theme={null}
# List tag definitions (bare `sequenzy tags` also lists)
sequenzy tags list
# Create a tag
sequenzy tags create vip
sequenzy tags create "VIP Customer" --color purple
# Update a tag's color
sequenzy tags update tag_abc123 --color red
# Delete a tag
sequenzy tags delete tag_abc123 --yes
```
Tag names are normalized to lowercase with hyphens, so `VIP Customer` becomes `vip-customer`. Colors include values like `gray` (the default), `red`, `green`, `blue`, and `purple`. System tags cannot be updated or deleted, and tags used by sequences cannot be deleted until those sequences stop referencing them. Deleting a tag removes it from every subscriber.
### Shopify
```bash theme={null}
# Show the store's browse-abandonment, cart-abandonment, and price-drop automation settings
sequenzy shopify settings get
sequenzy shopify settings get --json
# Turn features on or off
sequenzy shopify settings update --price-drop off
sequenzy shopify settings update --browse-abandonment on
# Fine-tune behavior (partial update - omitted flags keep their values)
sequenzy shopify settings update --browse-delay-hours 4 --browse-cooldown-hours 48
sequenzy shopify settings update --cart-abandonment on --cart-delay-hours 2
sequenzy shopify settings update --price-drop-min-percent 10 --price-drop-lookback-days 14
# Reset a section to the platform defaults
sequenzy shopify settings update --reset-price-drop
```
### Sync Rules
```bash theme={null}
# Show the effective sync rules (new workspaces start empty)
sequenzy sync-rules get
sequenzy sync-rules get --json
# Replace the full rule set from a file
sequenzy sync-rules update --rules-file ./sync-rules.json
# Replace it inline
sequenzy sync-rules update --rules-json '[{"triggerEvent":"ecommerce.order_placed","actions":{"addTags":["vinyl-collector"],"removeTags":[]},"conditions":{"purchasedProduct":{"tags":["Vinyl"]}}}]'
# Opt into the inherited SaaS/ecommerce platform preset
sequenzy sync-rules update --reset
```
Sync rules map events to automatic tag changes. `update` replaces the full rule set - fetch with `get --json`, edit, then send it back. Send an empty array to disable rules. `--reset` is an explicit opt-in to the legacy SaaS/ecommerce platform preset, not a neutral reset. Rules can carry conditions: required or excluded subscriber tags, and a product match for commerce events that tags buyers by product tag, collection, product type, or vendor. See [Sync Rules](/concepts/sync-rules) for the full model.
### Email Design System
```bash theme={null}
# Show the visual identity AI-generated emails render inside
sequenzy email-design-system get
sequenzy email-design-system get --json
# Adjust individual tokens (partial update)
sequenzy email-design-system update --button-shape pill --spine editorial
sequenzy email-design-system update --kicker-style letterspaced --opener-treatments editorial-masthead,title-led
# Return to the identity derived from brand context
sequenzy email-design-system update --reset
```
The design system fixes the company's visual grammar - kicker style (`chip` | `letterspaced` | `none`), title alignment, button shape, divider style, density, and sanctioned opener treatments - plus the composition spine (`hero-led` | `editorial` | `product-spec`) that anchors how emails are structured. It is stored as the company's design direction text: `update` rewrites that text (preserving custom prose below the new identity), and any token the text does not state is derived deterministically from brand context (`isDefault: true` in JSON output while the whole identity is derived). Adjustments apply to every future AI email generation and sequence enrichment. For free-prose direction edits use `sequenzy companies update --email-design-prompt`.
### Team
```bash theme={null}
# List team members and pending invitations
sequenzy team list
# Invite a team member
sequenzy team invite teammate@example.com --role admin
sequenzy team invite marketing@example.com --role marketer
sequenzy team invite editor@example.com --role restricted
sequenzy team invite finance@example.com --role viewer --billing-access
# Cancel a pending invitation
sequenzy team cancel-invitation inv_abc123 --yes
```
Existing Sequenzy users are added to the team immediately; everyone else receives an email invitation. Inviting and cancelling invitations requires owner or admin access. `marketer` members create, edit, and send campaigns and sequences and manage subscribers, but cannot open transactional emails, settings, billing, or the team; `restricted` members can open direct campaign links only. `--billing-access` can only be granted by the company owner and is not available for marketer or restricted members. Use `team list` to find invitation IDs.
### Inbox
```bash theme={null}
# List conversations
# The address anyone can email to reach your inbox
sequenzy inbox address
sequenzy inbox list
sequenzy inbox list --status open --unread
sequenzy inbox list --search "refund" --limit 50
# Get a conversation with its full message history
sequenzy inbox get conv_abc123
# Reply to a conversation
sequenzy inbox reply conv_abc123 --text "Thanks for reaching out!"
sequenzy inbox reply conv_abc123 --html-file ./reply.html --subject "Re: your question"
# Reply from a specific sender profile (see sequenzy sender-profiles list)
sequenzy inbox reply conv_abc123 --text "Happy to help" --sender-profile-id sp_abc123
# Add an internal note instead of emailing the subscriber
sequenzy inbox reply conv_abc123 --text "Customer asked for a refund" --note
# Triage
sequenzy inbox close conv_abc123
sequenzy inbox reopen conv_abc123
sequenzy inbox mark-read conv_abc123
sequenzy inbox mark-unread conv_abc123
# Attach files to a reply (repeat --attach)
sequenzy inbox reply conv_abc123 --text "Invoice attached" --attach ./invoice.pdf
# Forward one message, with its attachments, to someone else
sequenzy inbox forward conv_abc123 msg_abc123 --to sales@example.com
# Close or reopen several conversations at once (up to 100 IDs)
sequenzy inbox close conv_abc123 conv_def456 conv_ghi789
```
The inbox collects replies to campaigns, sequences, and transactional email, plus any email sent to your inbox address (`sequenzy inbox address`). Provide a reply body with `--text` and/or `--html-file`. Outbound replies are emailed to the subscriber and reopen closed conversations; messages sent with `--note` are internal-only and never reach the subscriber. Pass `--sender-profile-id` to send a reply from a specific verified sender profile; without it, the reply is sent from the API key owner's email address, which must be on one of your verified sending domains.
`--attach` adds a file to a reply; repeat it for up to 10 files and 15 MB total. `inbox get` lists each message's attachments with a signed download link when the file is stored. `inbox forward` emails a copy of one received or sent message (get message IDs from `inbox get`) and records it in the conversation without changing its status or unread state. `inbox mark-unread` flags a conversation unread again.
`inbox close` and `inbox reopen` accept up to 100 conversation IDs. With more than one ID they print how many conversations changed, how many already had the status, and which IDs were not found; the command exits with status 1 when any ID is not found. `--json` prints `updatedIds`, `unchangedIds` and `notFoundIds`.
### Webhooks
```bash theme={null}
# List webhook endpoints
sequenzy webhooks list
# Create a webhook endpoint
sequenzy webhooks create --name CI --url https://example.com/hook --event email.bounced
sequenzy webhooks create --name "All events" --url https://example.com/hook
# Update a webhook endpoint
sequenzy webhooks update wh_abc123 --url https://example.com/new-hook
sequenzy webhooks update wh_abc123 --event email.bounced email.complained
sequenzy webhooks update wh_abc123 --disable
# Delete a webhook endpoint
sequenzy webhooks delete wh_abc123 --yes
# Test and inspect deliveries
sequenzy webhooks test wh_abc123
sequenzy webhooks deliveries wh_abc123 --limit 50
sequenzy webhooks replay wh_abc123 del_456
```
Webhook payloads are signed. `webhooks create` prints the signing secret **once** at creation time; it cannot be retrieved later, so store it immediately to verify webhook signatures. Valid `--event` values cover `email.*`, `subscriber.*`, and `sequence.*` event types; omit `--event` to subscribe to the default event set.
On `webhooks update`, `--event` replaces the existing event list, and changing the URL or re-enabling with `--enable` resets the failure circuit breaker. Use `webhooks deliveries` to find delivery IDs before running `webhooks replay`.
### Products
```bash theme={null}
# List synced products (Stripe, Shopify, WooCommerce, api)
sequenzy products list --provider stripe
# Page through a catalog larger than one request
sequenzy products list --limit 100 --offset 100
# Fetch every page in one command
sequenzy products list --all
# Queue a Stripe product catalog sync
sequenzy products sync
sequenzy products sync --integration int_123
# Create or update a product (Commerce API, keyed by your productId)
# and attach its deliverable file in the same command
sequenzy products upsert my-ebook --title "The Ebook" --price-cents 1900 --currency USD --file ./ebook.pdf
# Bulk upsert products from a JSON file (up to 100)
sequenzy products upsert --products-file ./products.json
# Delete a product pushed via the Commerce API
sequenzy products delete my-ebook
# Upload a local file and attach it as the product's deliverable
sequenzy products attach-file prod_abc --file ./guide.pdf
# Attach an externally hosted file instead
sequenzy products attach-file prod_abc --url https://example.com/template.zip --name template.zip
# Remove the attached file
sequenzy products detach-file prod_abc
```
`products list` returns one page at a time (50 by default, 100 maximum). When the catalog is larger than the page, the header reads `Products (50 of 214)` and the output ends with the `--offset` value for the next page. Use `--all` to walk every page in one command.
`attach-file --file` (and `upsert --file`) uploads the file to Sequenzy storage (PDF, ePub, ZIP, images, audio, video, or text, up to 100MB) and attaches it in one step. `attach-file` and `detach-file` accept the product ID from `products list` or, for Commerce API products, your own `productId` (e.g. `sequenzy products attach-file my-ebook --file ./ebook.pdf`). After attaching, purchases of the product enrich the `saas.purchase` event with `download.url` and `download.name`, so purchase sequences can deliver the file with `{{event.download.url}}`. See [Digital Product Delivery](/concepts/digital-products).
`upsert` writes to the platform-agnostic [Commerce API](/concepts/custom-commerce) catalog: products are keyed by your own `productId`, and the same IDs are matched against order line items for purchase sequences and product filters.
### Account
```bash theme={null}
sequenzy account
sequenzy account --json
# Sending domains
sequenzy websites list
sequenzy websites add mail.example.com
sequenzy websites add mail.example.com --mail-from-prefix bounce
sequenzy websites check mail.example.com
sequenzy websites verify mail.example.com
# Company tracking domain, used by every sending domain
sequenzy tracking domain
sequenzy tracking domain set links.example.com
sequenzy tracking domain verify
sequenzy tracking domain remove --yes
```
`add` prints the DNS records to publish; the `Bounce:` line says whether to
publish one CNAME or the SPF TXT and MX records at the bounce subdomain. The `Tracking:` line is your company
tracking domain, shared by every sending domain and optional: sending never
waits for it, and links use Sequenzy's shared tracking domain until it
verifies. `--mail-from-prefix` chooses the bounce subdomain when the domain is
created. New domains publish that subdomain as one CNAME; domains added earlier
keep their SPF TXT and MX records. Set or change the tracking domain with `tracking domain set`; changing
it keeps links in emails you already sent working while the old CNAME stays.
`tracking domain remove` breaks those links, so it asks for confirmation.
`websites configure` and `--tracking-prefix` still work but are deprecated.
`check` and `verify` report DNS and sending readiness as two separate lines,
because they are two separate questions. `DNS: verified` means your records are
correct; `Sending: ready` means the domain can actually send. A domain can sit
at `DNS: verified` with `Sending: activating` while we finish activating it for
sending - that is normal for a few minutes after your records go live. The
reason is printed in parentheses, so you can tell activation apart from a DNS
problem instead of guessing from the record statuses, which describe DNS only.
### Notifications
```bash theme={null}
# What Sequenzy emails you about this workspace
sequenzy notifications get
sequenzy notifications get --json
# One summary a day instead of an email per signup
sequenzy notifications set --new-subscriber daily
# Email every form submission, including returning contacts
sequenzy notifications set --form-submitted instant
# Stop the Monday weekly report
sequenzy notifications set --weekly-report off
# Silence notifications
sequenzy notifications set --new-subscriber off --form-submitted off --campaign-completed off --weekly-report off
```
Instant form-submission notifications stop after 50 per workspace per UTC day. Submissions continue normally after the cap; only the account-notification email is suppressed. The weekly report is on by default for the workspace owner (off for invited members until they opt in) and only goes out for weeks in which the workspace sent more than 10 emails.
### Audit
```bash theme={null}
# Connected integrations and their sync health
sequenzy integrations list
sequenzy integrations list --all --json
# What a provider does, connected or not: events, attributes, caveats
sequenzy integrations catalog
sequenzy integrations catalog --provider stripe
sequenzy integrations catalog --category ecommerce
# One integration in depth, including which events nothing listens to
sequenzy integrations get int_123
# Recent webhook and sync activity (retained 24h)
sequenzy integrations activity --status failed
# What a built-in event payload contains, before you write {{event.*}} tags
sequenzy events schema
sequenzy events schema ecommerce.order_placed --provider shopify
sequenzy events schema ecommerce.browse_abandoned --example
# Sender (From) and reply-to profiles, with sending readiness
sequenzy sender-profiles list
# Rename one profile in place, leaving the account defaults alone
sequenzy sender-profiles update sender_abc123 --name "SnapCount"
sequenzy sender-profiles update reply_abc123 --name "SnapCount" --reply
# Permanently delete an unused sender (From) profile
sequenzy sender-profiles delete sender_abc123 --yes
# Open/click tracking, strict bot filtering, double opt-in, attribution window, tracking domain
sequenzy tracking settings
# Whether the workspace can send at all, and what to do if it cannot
sequenzy sending status
# Who a scheduled campaign will actually reach
sequenzy campaigns audience camp_123
```
### Diagnosing and restoring paused sending
Run `sending status` whenever a send, sequence step, or test send fails for a
reason that is not a validation error. A company-level sender-health pause blocks
every send, including test sends, and it is reported here rather than on the
individual send:
```bash theme={null}
sequenzy sending status
sequenzy sending status --json
```
The output gives the pause reason, the measured permanent-bounce, temporary-bounce,
and complaint rates against the thresholds that apply at your volume, the state of
the automated sender-health review, whether one-click resume is available, and
ordered remediation steps. Reading it needs only `account:read`, so a read-only
monitoring key can diagnose a blocked workspace.
`metricsWindow` is the part people get wrong: enforcement uses all-time totals
counted from a reset watermark, **not** a rolling window. `expiresAt` is always
`null`. A paused rate does not decay, so waiting never restores sending.
Once the cause is fixed and `selfResume.canSelfResume` is `true`:
```bash theme={null}
sequenzy sending resume --confirm-list-sanitized
```
`--confirm-list-sanitized` is required and is recorded on the account's audit
trail, so pass it only after the fix is live. Resume needs `companies:manage`
plus owner or admin access, only clears a permanent-bounce pause, never removes
suppressions, and requires the automated review to have cleared first. When it
cannot proceed, the error names the blocking gate. On success the bounce
watermark moves to now and the service attempts to requeue paused campaigns plus
due sequence steps. If that handoff is only partial, the success message gives
recovery guidance. See [Sender Health](/guides/sender-health).
To change how the account measures engagement, use `tracking update`:
```bash theme={null}
# Account-wide opt-out from open and click tracking
sequenzy tracking update --opens off --clicks off
# Route unsubscribe links directly to sequenzy.com, even with a custom domain
sequenzy tracking update --unsubscribes off
# Opt in to aggressive bot filtering (can lower reported open/click rates)
sequenzy tracking update --strict-bot-filtering on
# Longer revenue attribution window (1-720 hours)
sequenzy tracking update --attribution-window-hours 168
# Require new contacts to confirm before they become subscribed
sequenzy tracking update --double-opt-in on
# Send subscribers to your own page after they confirm
sequenzy tracking update --double-opt-in-redirect-url https://example.com/welcome
# Automatic UTM tagging
sequenzy tracking update --auto-utm on --utm-source newsletter
sequenzy tracking update --reset-utm
```
`--unsubscribes off` changes unsubscribe link routing for subsequent sends,
Actual unsubscribes and their email attribution are still recorded.
`tracking update` is a partial update: omitted flags keep their current values.
It applies to emails sent afterwards - already-sent emails keep the links and
pixels they were rendered with. Reply tracking is set with
`sequenzy companies update`, and the company tracking domain with
`sequenzy tracking domain`. The command needs a key with `companies:manage`.
`--double-opt-in on` requires a sender profile and provisions the confirmation
email automatically when the account does not have one yet. New contacts from
forms, the API, and integrations then start pending until they confirm;
contacts that are already active are not changed.
`--double-opt-in-redirect-url` sets where the hosted confirmation page sends
subscribers after they confirm; pass an empty string (`""`) to clear it and
keep them on the branded confirmation page. See
[Double Opt-In](/concepts/double-opt-in).
The integration commands never return credentials, access tokens, or webhook
secrets. `integrations list` prints `Unusable records` for a store
sync that could not import every customer - typically an old order on a domain
that has since been shut down. Those buyers are imported as suppressed profiles
so their revenue still counts while nothing is sent to them; use `--json` for
the full list.
`integrations catalog` describes providers whether or not they are connected, so
you can compare them before picking one. Coverage differs in ways that matter:
only Shopify has storefront browse tracking, only Stripe and Chargebee cover the
full trial lifecycle, and Polar emits no payment-failure event at all - so a
dunning sequence built on Polar would never fire.
`events schema` answers the question that follows: `integrations catalog` names
the events a provider emits, and this shows what is inside one. It prints a real
example payload per provider plus every property path, its type, and the merge
tag that resolves it - so `{{event.*}}` tags can be checked rather than guessed,
which matters because an unrecognized merge tag renders as an empty string
instead of an error. It also spells out the traps the sample cannot: `*Cents`
fields are minor units, `price` is a preformatted display string, and `orderId`
is a string on Shopify but a number on WooCommerce. Custom event names have no
published payload, and that is not an error - they carry exactly the properties
you send.
`integrations get` crosses those events with your own configuration. For each
event it shows the tags your sync rules apply, any conditions gating that rule,
and which sequences trigger on it. Events with no sequence are called out
explicitly, which is usually the fastest way to find automation you have not
built yet.
Every event also reports whether it has actually been received. The catalog
describes what a provider *can* send; `Received:` tells you what has arrived.
The gap is diagnostic - an integration taking purchases but that has never once
seen `saas.trial_started` usually means trials are not configured or the
provider's webhook is missing that topic, not that the integration is broken.
That failure is otherwise invisible, because the integration reports healthy.
API-key and webhook-secret providers can be connected from the terminal:
```bash theme={null}
# A webhook-secret-only provider
sequenzy integrations connect clerk --webhook-secret whsec_...
# Payment providers: API key + webhook secret + account id
sequenzy integrations connect paddle --api-key pdl_... --webhook-secret pdl_ntf_... --account-id 12345
sequenzy integrations connect polar --api-key polar_oat_... --webhook-secret whsec_...
# PostHog, with an optional event-history import
sequenzy integrations connect posthog --webhook-secret shared_secret \
--history-region us --history-project-id 12345 --history-api-key phx_...
# Segment, optionally scoped to specific event names
sequenzy integrations connect segment --webhook-secret segment_shared_secret \
--event-allowlist "Order Completed" signed_up
# Segment, with an optional event-history import from the Unify Profile API
sequenzy integrations connect segment --webhook-secret segment_shared_secret \
--history-region us --history-space-id spa_abc123 --history-api-key profile_token
```
`connect` covers Polar, Paddle, Dodo, Lemon Squeezy, Whop, Creem, Chargebee, Clerk, PostHog,
Segment, and Affonso - run `integrations catalog --provider ` first, its
`connectFields` list exactly what each provider needs. Secrets can come from
`SEQUENZY_INTEGRATION_API_KEY`, `SEQUENZY_INTEGRATION_WEBHOOK_SECRET`,
`SEQUENZY_POSTHOG_PERSONAL_API_KEY`, and `SEQUENZY_SEGMENT_PROFILE_API_TOKEN`
instead of flags, keeping them out of shell history. The response prints the webhook URL to configure at the
provider with the same secret. OAuth and app-install providers (Stripe,
Shopify, Supabase, GitHub, WooCommerce) still connect in the dashboard.
For Lemon Squeezy, run `sequenzy integrations disconnect ` to stop ingestion
and remove the managed webhook. Inspect `cleanupWarning` in `--json` output;
repeat disconnect to retry provider cleanup after a warning. Other providers
disconnect from the dashboard. The command needs a key with `integrations:manage`.
To manage sync without disconnecting:
```bash theme={null}
# Pause bulk imports and backfills, keeping credentials and live delivery
sequenzy integrations disable-sync int_123
sequenzy integrations enable-sync int_123
# Re-run a payment provider's customer and revenue sync
sequenzy integrations sync int_123
# Backfill existing Supabase users into subscribers
sequenzy integrations sync int_supabase
# Retry a PostHog or Segment event-history import that failed or was interrupted
sequenzy integrations sync int_posthog
sequenzy integrations sync int_segment
```
`disable-sync` stops future bulk imports and backfills for providers whose
catalog actions expose sync controls, but keeps the connection, credentials,
and live webhook delivery active. An import already in progress must finish
first.
`disable-sync` does **not** stop an integration creating contacts. Its live
webhook keeps writing every new signup or customer as it happens. What you can
control is where those contacts land:
```bash theme={null}
# Choose which lists this integration's new contacts join
sequenzy integrations set-lists int_supabase --list list_signups
sequenzy integrations set-lists int_supabase --none
sequenzy integrations set-lists int_supabase --default
```
`set-lists` decides which lists contacts created by the integration join on
future provider writes. Nothing is applied retroactively and nobody is ever
removed from a list. Wix/Webflow submissions, Shopify customer updates, and
Supabase resubscriptions can add existing contacts to the new targets. Stripe
applies targeting only when its webhook creates a subscriber. `--none` and
`--default` are different: `--none` means new contacts join no list at all,
while `--default` falls back to the workspace default lists setting. Supported
for Supabase, Stripe, Shopify, Wix, and Webflow.
This changes list membership only. Contacts are still created, their attributes
still sync, sync-rule tags still apply, and default `any_contact` sequences
still enroll them precisely because the contact joined no list. Explicit
`any_list` and specific-list sequences wait for a matching membership and do
not enroll a list-less contact. Pair the targeting change with `sequences
pause-enrollments` if you need `any_contact` enrollment stopped too. To stop a provider feeding the
workspace entirely, use `integrations disconnect ` for Lemon Squeezy.
For other providers, disconnect from the dashboard, which also runs provider cleanup.
`integrations get` prints the current bulk sync state and list targeting in its
`Ingestion` section, with the names of any targeted lists. `set-lists` needs a
key with `integrations:manage`.
`sync` is supported for
Stripe, Polar, Paddle, Dodo, Lemon Squeezy, Creem, Chargebee, Whop, Supabase, PostHog, and
Segment; it
returns immediately, so poll `integrations get` to watch `syncStatus`. The
Supabase run backfills users from the table already configured for that
integration and cannot be pointed at another one - worth running once after
connecting, since the database trigger only sends rows that change after it is
installed. The PostHog and Segment runs re-import event history with the
credentials stored at connect time, and are the supported way to retry an
import that failed or was interrupted: they restart from the beginning, and
already-imported events dedupe, so a re-run cannot duplicate them. Both are
refused while an import is still reporting progress. Both
need a key with
`integrations:manage`. Lemon Squeezy supports both connect and disconnect
through the CLI; OAuth connections and other provider disconnects use the dashboard.
For Shopify, the storefront tracking pixel has its own pair of commands:
```bash theme={null}
# Is on-site tracking actually live on the store?
sequenzy integrations pixel int_123
# Install it, or repoint an existing pixel at this account
sequenzy integrations enable-pixel int_123
```
The pixel is what produces product views, cart activity, and browse
abandonment. Without it those events never arrive, so a browse-abandonment or
cart-recovery sequence built on them never fires and nothing reports an error.
`pixel` reads the store directly on every call, because a merchant can remove
the pixel from the Shopify admin without Sequenzy hearing about it. Check it
before promising that on-site tracking works, and again after connecting a
store. `enable-pixel` is idempotent and reports `changed: false` when the pixel
is already live; events start arriving on the next storefront visit and nothing
is backfilled for the period it was off. `pixel` needs `account:read`,
`enable-pixel` needs `integrations:manage`. `integrations get` prints the same
state in its `Storefront pixel` section.
For Attio, list mapping after connect does not need the access token again:
```bash theme={null}
# Sequenzy lists, live Attio people-lists, and the saved map
sequenzy integrations attio int_123
# Map lists (full replacement) without reconnecting
sequenzy integrations set-attio-lists int_123 --map list_abc=field-guide list_def=benchmark
```
`mappedListCount` 0 means list joins are not synced. `attio` needs `account:read`
and `lists:read`; `set-attio-lists` needs `integrations:manage`. See
[Attio](/integrations/attio).
Sites that are not Shopify use a publishable key and a script tag instead of a
pixel. `web-tracking` manages those keys:
```bash theme={null}
# Create a key restricted to your own origins, and print the snippet to install
sequenzy web-tracking create --name "Storefront" \
--origins https://example.com,https://*.example.com
sequenzy web-tracking list
sequenzy web-tracking get wtk_123
# Replace the allowlist, or revoke without deleting
sequenzy web-tracking update wtk_123 --origins https://example.com
sequenzy web-tracking update wtk_123 --deactivate
```
`create` prints the exact `
```
With a bundler:
```ts theme={null}
import { createBrowserClient } from "@sequenzy/web-sdk";
const sequenzy = createBrowserClient({
publicKey: "seq_pk_your_key",
companyId: "your_workspace_id",
});
const result = await sequenzy.subscribeToPush();
// { status: "subscribed", deviceId } | { status: "denied" } |
// { status: "unsupported" } | { status: "disabled" } | { status: "error", error }
```
`isPushSupported()` and `getPushPermission()` let you decide whether to show
the button at all. Pass `{ serviceWorkerPath: "/path/to/sw.js" }` if your
worker lives somewhere other than `/sequenzy-sw.js`.
### Linking browsers to contacts
A browser that subscribes before the visitor signs in is anonymous and does
not receive campaigns yet. When your site calls
`sequenzy.identify(email, identityToken)` (see
[web tracking](./web-tracking#step-3-identify-your-visitors)), the SDK re-registers the
subscription with that identity and the device is linked to the contact. The
SDK also refreshes the registration once a day and whenever the browser's
push endpoint changes.
When a visitor signs out, call `sequenzy.reset()`. The SDK unregisters the
browser and drops the contact link, so the next person on a shared computer
does not receive the previous person's notifications.
## iOS and Android apps
### Credentials
In the **iOS** and **Android** tabs of **Settings → Push**, **Copy prompt** gives
your AI coding tool or developer the app-side steps below with your bundle ID or
Firebase project filled in. Then add credentials:
* **iOS**: upload the `.p8` auth key from Apple Developer (Keys, with Apple
Push Notifications service enabled) and enter the key ID, team ID, and bundle
ID. Choose **Sandbox** for Xcode development builds and **Production** for
App Store and TestFlight builds.
* **Android**: upload the service account JSON from Firebase project settings
(Service accounts, Generate new private key). The account needs the Firebase
Cloud Messaging API Admin role.
Keys are encrypted and never shown again. If Apple or Google rejects them, the
settings page shows the error until a send succeeds or you replace the key.
### Register device tokens
Your app gets a token from APNs or Firebase Messaging. Send it to your backend
and register it with [Register a push device](../api-reference/push/register-device):
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/devices" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"token": "8f2c9e...",
"email": "jane@example.com",
"deviceName": "Jane iPhone",
"appVersion": "3.2.0"
}'
```
Register the token again whenever the app receives a new one; the same token
is updated rather than duplicated, and moves to the contact you pass. When a
person signs out of your app, call
[Unsubscribe a push device](../api-reference/push/delete-device). Each contact
keeps up to 20 active devices; older ones are retired automatically.
### What your app receives
Each notification carries a `sequenzy` object: in the APNs payload next to
`aps`, and as string fields in the FCM `data` map.
```json theme={null}
{
"aps": {
"alert": { "title": "Your order shipped", "body": "Tap to track it" },
"sound": "default"
},
"sequenzy": {
"v": 1,
"title": "Your order shipped",
"body": "Tap to track it",
"url": "myapp://orders/123",
"imageUrl": "https://cdn.example.com/box.png",
"pushSendId": "push_abc",
"deviceId": "dev_123",
"clickToken": "Qm9...",
"eventsUrl": "https://api.sequenzy.com/api/v1/push/events"
}
}
```
* Open `url` when the person taps the notification. It can be an https link or
one of your app's deep links.
* iOS shows images only through a Notification Service Extension that
downloads `imageUrl`. Sequenzy sets `mutable-content` whenever an image is
present.
* To report opens, POST to `eventsUrl` from the app:
```json theme={null}
{
"pushSendId": "push_abc",
"deviceId": "dev_123",
"token": "Qm9...",
"type": "clicked"
}
```
Use `"type": "delivered"` from a Notification Service Extension (iOS) or when
your Android app handles the message, to count displays. The token
authenticates the report, so no API key belongs in your app.
## Writing a push
| Field | Notes |
| - | - |
| Title | Up to 120 characters. Most devices show about 40. |
| Message | Up to 500 characters. Most devices show about 120. |
| Link on tap | An https URL, an app deep link, or a merge tag that resolves to one. Browsers ignore deep links. |
| Image | Optional https image. |
| Icon | Optional https icon for web push. Defaults to the icon in push settings, then your company logo. |
| Platforms | Web, iOS, and Android by default. Limit a message to some platforms if the copy only fits one. |
Merge tags work in the title, message, and link, for example
`Hi {{FIRST_NAME|there}}`. If a merged message grows past the 4 KB limit push
services accept, Sequenzy shortens the message rather than failing the send.
## Push campaigns
Open `Campaigns`, switch to **Push**, and choose **New Push Campaign**. Write
the notification, pick the audience, then send now or schedule it. Only
contacts with an active device on a selected platform receive the campaign;
the recipients step shows how many that is. The audience is re-evaluated when
delivery starts, so contacts who subscribe before a scheduled send are
included.
A contact receives one notification per active device. If a push service is
temporarily unavailable, Sequenzy retries only the devices that have not
accepted the message yet.
## Segments
Push filters appear in the segment builder once any platform is set up:
`pushDevice` (has an active device, on any platform or a specific one) and
`pushSent`, `pushDelivered`, and `pushClicked` with a time window such as
`30d` or `all`. For example, combine `pushDevice is ios` with
`pushClicked is_not 30d` to find iOS users who stopped opening your
notifications. See [segment filters](./subscribers#push-filters).
## Push steps in sequences
Add a **Send Push** step anywhere in a sequence. If the contact has no active
device, the step is skipped by default; set **If the contact has no push
device** to **Exit the sequence** to stop the journey instead. A workspace
without push set up always skips the step, so a missing key never exits
contacts in bulk.
## Stats
| Metric | Meaning |
| - | - |
| Sent | Contacts whose notification at least one push service accepted |
| Devices | Devices that accepted, out of devices targeted |
| Displayed | Contacts whose device reported showing it. Browsers report this automatically; apps report it through `eventsUrl` |
| Clicked | Contacts who opened the notification |
| Failed | Every device rejected the push, or the credentials were refused |
| Skipped | No active device on a set-up platform, or the campaign was stopped |
Test sends are excluded from stats and from contact activity.
## Test sends
**Settings → Push → Send a test push** sends to one device or to every device
of a contact. You can send 200 test pushes per workspace in a rolling 24 hours.
## API, CLI, and MCP
Everything in the dashboard is also available programmatically:
* **API**: [push settings](../api-reference/push/settings), web, iOS, and
Android credentials, [devices](../api-reference/push/list-devices),
[test sends](../api-reference/push/test), and
[push campaigns](../api-reference/push/create-campaign). Sequence steps use
`type: "push"` in [Create a sequence](../api-reference/sequences/create).
* **CLI**: `sequenzy push settings`, `push web --enable`, `push ios set`,
`push android set`, `push devices list|register|remove`, `push send-test`,
and `push campaigns estimate|create|update|send|cancel|stats`.
* **MCP**: `get_push_settings`, `register_push_device`, `send_test_push`,
`create_push_campaign`, `send_push_campaign`, and the other push tools listed
in [MCP](./mcp#push-notifications).
Push campaign scopes reuse the campaign API key scopes (`campaigns:read`,
`campaigns:write`, `campaigns:send`); test sends need `campaigns:send`;
devices use `subscribers:read` and `subscribers:write`; reading push settings
needs `account:read` and changing credentials needs `integrations:manage`.
## Browser endpoints
The web SDK calls these public endpoints with your publishable key and the
allowed origins configured on it. You only need them if you build your own
subscription flow:
| Endpoint | Purpose |
| - | - |
| `GET /api/v1/push/web-config?companyId=&publicKey=` | Returns whether web push is on and the VAPID public key |
| `POST /api/v1/push/web-subscriptions` | Registers a `PushSubscription` (`subscription`, optional `anonymousId`, and `email` plus `identityToken` to link a contact) |
| `POST /api/v1/push/web-subscriptions/remove` | Unregisters an endpoint and drops its contact link |
| `POST /api/v1/push/events` | Records `delivered` and `clicked` reports authenticated by the notification's token |
Send bodies as JSON with `Content-Type: text/plain` to avoid a CORS preflight.
## Troubleshooting
* **Nobody is eligible**: check **Settings → Push** for device counts. Browsers
that subscribed before sign-in stay anonymous until your site calls
`identify`.
* **iOS sends fail with "Key rejected"**: the key ID, team ID, bundle ID, or
environment does not match the app build. Development builds need
**Sandbox**.
* **Android sends fail with "Key rejected"**: the service account is missing the
Firebase Cloud Messaging API Admin role, or the key was deleted in Google
Cloud.
* **A browser never prompts**: `subscribeToPush()` must run from a click, the
page must be https, and the visitor must not have blocked notifications for
your site.
# Test push notifications
Source: https://docs.sequenzy.com/concepts/push-testing
Set up and verify web, iOS, and Android push notifications end to end
Use this guide to verify one device on each push platform before testing a
campaign or automation. Use a staging workspace and a test subscriber. Never
ship API keys, APNs private keys, or Firebase service-account JSON in a mobile
app or browser bundle.
## Test checklist
For every platform, verify all of these:
1. The device is registered and is counted on its platform tab in **Settings → Push** and listed in **Send a test push**.
2. **Send a test push** reports that the provider accepted the notification.
3. The device displays the title, body, image, and icon where supported.
4. Tapping the notification opens the expected HTTPS URL or app deep link.
5. The device's delivered and clicked activity is recorded.
6. Unregistering the device prevents later sends.
## Web
### Setup
Enable web push in **Settings → Push → Web**. Add the SDK to a real HTTPS
site, or use `localhost` for development:
```html theme={null}
```
Serve `/sequenzy-sw.js` from the same origin as the page:
```js theme={null}
importScripts("https://api.sequenzy.com/sequenzy-sw.js");
```
The permission request must be triggered by a user click. After clicking the
button, check DevTools:
* `Notification.permission` is `granted`.
* An activated service worker controls the page.
* `registration.pushManager.getSubscription()` returns a subscription.
* The subscription registration request returns HTTP `200` or `201`.
### Web verification
Send a test push to the registered device. Confirm the notification arrives
after closing or backgrounding the page, then click it and verify the target
URL. Test again after calling `sequenzy.reset()` and confirm the device is
unlinked from the previous contact.
## iOS with APNs
### Provider setup
In **Settings → Push → iOS**:
1. Create an Apple Push Notification authentication key with Push Notifications
enabled.
2. Upload the `.p8` file.
3. Enter the Apple key ID, Team ID, and app bundle ID.
4. Select **Sandbox** for an Xcode development build and **Production** for
TestFlight or App Store builds.
The `.p8` file is uploaded to the dashboard and must not be included in the
application.
### App registration
Enable remote notifications and forward the APNs token to your backend. Your
backend registers it with the Sequenzy API:
```swift theme={null}
import UIKit
import UserNotifications
final class PushRegistration: NSObject, UNUserNotificationCenterDelegate {
func requestPermission() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
guard granted, error == nil else { return }
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
// Send `token` to your authenticated backend, never directly with an
// API key embedded in the app.
}
}
```
Register the token server-side:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/devices" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"token": "APNS_HEX_DEVICE_TOKEN",
"email": "test@example.com",
"deviceName": "QA iPhone"
}'
```
### iOS verification
Use a physical device, or the iOS Simulator on an Apple silicon Mac with
Xcode 14 or later, which receives Sandbox pushes. Confirm the APNs environment
matches the credential and build, then send a test push. Test both foreground
and background states, tap the notification, and verify the app opens the
supplied deep link.
If the notification contains an image, add a Notification Service Extension
and download the image when `mutable-content` is present. Report `delivered`
and `clicked` events to the `eventsUrl` with the token included in the payload,
as described in [Push notifications](./push-notifications).
## Android with FCM
### Provider setup
In **Settings → Push → Android**:
1. Open the Firebase project used by the Android application.
2. Generate a service-account JSON file under **Project settings -> Service
accounts**.
3. Upload the JSON file and confirm the project ID shown by Sequenzy.
The service-account JSON belongs only in the dashboard or a secure backend.
### App registration
Add Firebase Messaging to the Android app and forward the token to your
backend:
```kotlin theme={null}
class PushMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// POST the token to your authenticated backend.
// The backend calls Sequenzy's register-device endpoint.
}
override fun onMessageReceived(message: RemoteMessage) {
val data = message.data
val title = data["title"] ?: return
val body = data["body"].orEmpty()
// Show a notification and retain pushSendId, deviceId, clickToken,
// and eventsUrl for delivered/clicked reporting.
}
}
```
Register the token server-side:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/push/devices" \
-H "Authorization: Bearer seq_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"platform": "android",
"token": "FCM_REGISTRATION_TOKEN",
"email": "test@example.com",
"deviceName": "QA Pixel"
}'
```
### Android verification
Use a physical device or an emulator with Google Play services. Test the app
in foreground, background, and after force-closing it. In the foreground,
`onMessageReceived` runs and your app shows the notification. In the
background, Android shows it for you, and a tap opens your launcher activity
with the payload fields (`pushSendId`, `deviceId`, `clickToken`, `eventsUrl`,
`url`) as intent extras. Verify notification tap
handling, deep-link routing, and delivered/clicked reporting. Rotate the FCM
token and confirm the second registration updates the existing device instead
of creating an uncontrolled duplicate.
## Provider failure checks
After the happy path works, test these failure cases in staging:
* Replace APNs or FCM credentials with an invalid key and confirm sends fail
with a useful credential error.
* Uninstall the app or invalidate a browser subscription and confirm the device
becomes inactive after provider rejection.
* Retry a send after a provider error and confirm an accepted device does not
receive the same push twice.
* Remove the device and confirm it is excluded from the next campaign.
# Inbox
Source: https://docs.sequenzy.com/concepts/reply-tracking
Receive replies and inbound email in one threaded inbox for your team and your AI agents
The Inbox collects two kinds of email in threaded conversations:
* **Replies** to your campaigns, sequences, and transactional emails
* **Anything sent to your inbox address**, from people, services, or other AI agents
Your team works the inbox in the dashboard. AI agents can do the same through the REST API, the CLI, and the MCP server: list unread conversations, read threads and attachments, reply, forward, mark read or unread, and close.
## Turning on the Inbox
Navigate to **Settings → Inbox** and turn on **Receive email in Inbox**. Once enabled, all outbound emails use a trackable reply address that routes responses back to Sequenzy, and your inbox address starts accepting mail.
## Your Inbox Address
Every workspace has an inbox address:
```text theme={null}
inbox+{companyId}@inbound.sequenzy.com
```
It is shown at the top of the Inbox page and in **Settings → Inbox**, and returned by [Get Inbox Address](../api-reference/conversations/inbox-address), `sequenzy inbox address`, and MCP `get_inbox_address`.
### Use your own domain
You can receive at a branded address such as `support@inbound.acme.com` instead. In **Settings → Inbox**, choose **Use your domain**, enter a name and pick a verified sending domain. The address lives on the `inbound.` subdomain, so your existing mailboxes on `acme.com` are not affected. Add the MX record shown there; the address starts receiving once it is verified, and the default address keeps working either way. Changing or removing an active branded address stops the old one immediately. The same is available through [Update Inbox Address](../api-reference/conversations/update-inbox-address), `sequenzy inbox address --local-part support --domain acme.com`, and MCP `update_inbox_address`.
Email sent to the inbox address:
* Opens a new conversation, or joins an existing one when its `In-Reply-To` or `References` headers point at a message in that conversation
* Links to a subscriber when the sender's address matches one; senders are not added as subscribers
* Does not fire `email.replied` webhooks or automations, is not counted in reply analytics, and is not auto-forwarded
* Is dropped when the SES spam or virus scan fails, and is not linked to a subscriber or threaded when its DMARC check fails
Once your team replies, the contact's answers come back through the reply's tracked Reply-To address, so from then on they behave like any reply, including `email.replied` webhooks, automations and auto-forwarding.
You can forward mail from another mailbox to your inbox address, for example `support@yourdomain.com`, to collect it in Sequenzy.
### Configuration Options
| Setting | Description |
| - | - |
| **Receive email in Inbox** | Enable/disable reply capture and the inbox address |
| **Reply-to Domain** | Choose whether trackable reply addresses use Sequenzy's inbound domain or your verified sending domain |
| **Auto-forward replies** | Automatically forward replies to the original sender address |
| **Retention** | Days received email and attachments are kept, at least 90 (returned as `replyRetentionDays`) |
### Reply-to Domains
By default, Sequenzy uses `reply+{emailSendId}@inbound.sequenzy.com` for trackable reply addresses. This requires no DNS setup beyond enabling reply tracking.
If you want replies to use your own domain, choose **Your domain** in the Reply-to Domain setting. Sequenzy will use addresses like `reply+{emailSendId}@inbound.yourdomain.com` after:
* Your sending domain is verified
* The inbound MX record for `inbound.yourdomain.com` is verified
* The reply domain or one of its parent domains is verified in the receiving AWS SES region
* The inbound SES receipt rule is active
If the custom route is not ready, Sequenzy automatically falls back to `inbound.sequenzy.com` so replies remain trackable.
SES reply configurations for a sender such as `news.example.com` may use
`inbound.news.example.com` or retain the legacy `inbound.example.com`. Use the
configured reply domain shown in your settings.
### Replies bounce even though the MX record is verified
Your MX record routes mail to SES, but SES also needs a verified identity for
the receiving domain or a parent domain in that same region. Verifying
`news.example.com` covers `inbound.news.example.com`; it does not cover the
sibling `inbound.example.com`.
If your existing reply domain needs separate verification, refresh its DNS
status after the inbound MX verifies. The **Reply tracking DNS** section shows
the exact verification TXT record to publish. Publish it and refresh again
after DNS propagation. Keep the TXT in DNS after verification; the record
remains visible for reference. Keep your existing reply domain and MX
record so replies to previously delivered emails can still reach you.
Confirmed unverified receiving identities leave custom routing inactive. New
sends use the Sequenzy reply domain until verification succeeds. If the SES
verification check is temporarily unavailable, an already active route stays
active so you can continue receiving replies; an inactive route stays inactive.
Older SES routes display pending until receiving ownership has been checked.
An inconclusive check keeps an existing route accepting replies and retains
previously confirmed ownership, without marking an unchecked route as active.
You can use the same recovery flow through
[Verify Sending Domain](/api-reference/websites/verify),
`sequenzy websites verify `, or MCP `verify_sending_domain`.
Read `website.dnsRecords.inboundRoutingStatus` separately from `readyToSend`.
When `inboundVerificationRecord` is returned with a pending
`inboundVerificationStatus`, publish its TXT `name` and `value`, then verify
again. `websites check` and MCP `check_website` read the stored records.
If a parent domain becomes verified, an unnecessary pending TXT instruction
is removed automatically. These generated fields cannot be set or cleared
through the public API.
Retry domain verification to refresh its status.
Changing the reply-to domain setting does not change addresses in emails you
have already sent. Replies that already bounced must be resent after the route
is repaired.
## How Reply Tracking Works
1. **Outbound emails** include a unique reply address in the format `reply+{emailSendId}@inbound.sequenzy.com` or `reply+{emailSendId}@inbound.yourdomain.com`
2. When a subscriber hits reply, their email is routed to Sequenzy
3. The reply is **parsed** to extract the new content (quoted text is stripped)
4. A **conversation** is created or updated with the new message. Replies to your team's replies stay in the same conversation
5. The conversation is marked unread in the Inbox and you can respond directly
## Viewing Conversations
Access your conversations from the **Inbox** in the left navigation. The tabs show:
* **All**: Every conversation
* **Unread**: Conversations with new messages
* **Open**: Conversations awaiting your response
* **Closed**: Resolved threads
Use the envelope button on a row, or **Mark unread** in a conversation, to flag it unread again. Marking a conversation unread from inside it returns you to the Inbox so opening it does not immediately mark it read.
To close or reopen several conversations at once, tick the checkbox on each row, or the checkbox above the list to select the whole page, then choose **Close** or **Reopen** in the bar that appears.
### Conversation Details
Click any conversation to see:
* Full message thread with timestamps and attachments
* Subscriber profile and engagement history
* Original email that started the conversation, or **Inbox address** for mail sent there
* Reply, forward, mark unread, and close actions
## Replying to Subscribers
You can reply directly from Sequenzy:
1. Open a conversation from the Inbox
2. Check the **From** address and pick another sender profile if needed
3. Type your response in the composer
4. Click **Send reply**, or press `Cmd+Enter` (`Ctrl+Enter` on Windows)
The **From** address starts as the sender your team last replied from in that conversation. If your team has not replied yet, it starts as the sender of the email the subscriber replied to when that address is one of your sender profiles, and otherwise as your default sender. Only sender profiles whose domain is ready to send are listed, and **Manage senders** opens your domain settings. Through the API, CLI or MCP, pass `senderProfileId` (`--sender-profile-id` in the CLI) to choose the sender.
Your reply is sent from your verified sending domain and appears in the subscriber's email client as a normal reply. The conversation thread is maintained with proper email headers, so it displays correctly in Gmail, Outlook, and other email clients.
## Attachments
Attachments on received email are stored privately and can be downloaded from the conversation. The API returns a signed `downloadUrl` for each stored file, valid for one hour. You can attach files to replies with the paperclip in the composer, `attachments` in the API, `--attach` in the CLI, or `attachments` in MCP `reply_to_conversation`: up to 10 files and 15 MB per reply.
A received file keeps its name but cannot be downloaded when it failed the virus scan, is larger than 25 MB, is beyond the first 20 files of a message, could not be stored, or has passed the retention period. A reply fails, rather than going out without it, when a file you attached cannot be sent. Attachment storage must be configured on the server; `attachmentsAvailable` from [Get Inbox Address](../api-reference/conversations/inbox-address) tells you whether it is.
## Forwarding a Message
Forward any received or sent message, with its attachments, to another address with the forward button on the message, [Forward Conversation Message](../api-reference/conversations/forward), `sequenzy inbox forward`, or MCP `forward_conversation_message`. The forward appears in the thread with its delivery status and does not change the conversation's status or unread state. Notes cannot be forwarded. Files that are not stored, cannot be read, or do not fit the 15 MB email limit are left out, and the forwarded email lists them. A forward counts as a sent email and triggers the `email.sent` webhook, and replies to it are not added to the conversation.
## Reply Forwarding
If you prefer to handle replies in your regular email client, enable **Auto-forward replies** in **Settings → Inbox**. When enabled:
* Replies are forwarded to the `replyTo` address from the original email
* The forwarded email includes the subscriber's message and context
* You can reply directly from your email client
For transactional API sends, reply tracking changes how the outgoing `Reply-To`
header is used. Recipients see a unique trackable reply address, while Sequenzy
stores the original destination for forwarding:
* Template sends forward to the API `replyTo` value when provided, otherwise to
the transactional template's reply profile
* Direct-content sends forward to the API `replyTo` value when provided
* Direct-content sends that omit `replyTo` forward to the company's default
reply profile, or the first reply profile in the company if no default is set
* If no forwarding destination is available, replies are still captured in the
Sequenzy inbox but are not forwarded to an external inbox
Even with forwarding enabled, replies are still captured in Sequenzy for
analytics and record-keeping.
Reply forwarding affects inbound replies only. It does not route outbound
campaign, sequence, or transactional delivery through the forwarding mailbox's
SMTP provider.
## Reply Analytics
Reply metrics are available alongside opens and clicks:
* **Reply rate**: Percentage of delivered emails that received a reply
* **Total replies**: Count of inbound messages
* **Per-campaign metrics**: See which campaigns generate the most engagement
View reply metrics in:
* Campaign analytics (after sending)
* Sequence node performance
* Overall metrics dashboard
## API Access
The inbox is available through the REST API, CLI, and MCP server, so an AI
agent can work it the same way your team does. Outbound replies are queued asynchronously; a successful create
response initially reports `deliveryStatus: "pending"`.
| Workflow | REST API | CLI | MCP |
| - | - | - | - |
| List conversations | `GET /api/v1/conversations` | `sequenzy inbox list` | `list_conversations` |
| Read a thread | `GET /api/v1/conversations/{id}` | `sequenzy inbox get ` | `get_conversation` |
| Reply or add a note | `POST /api/v1/conversations/{id}/messages` | `sequenzy inbox reply ` | `reply_to_conversation` |
| Close or reopen | `POST /api/v1/conversations/{id}/status` | `sequenzy inbox close ` / `reopen ` | `update_conversation_status` |
| Close or reopen many | `POST /api/v1/conversations/bulk/status` | `sequenzy inbox close ...` / `reopen ...` | `bulk_update_conversation_status` |
| Mark read | `POST /api/v1/conversations/{id}/read` | `sequenzy inbox mark-read ` | `mark_conversation_read` |
| Mark unread | `POST /api/v1/conversations/{id}/unread` | `sequenzy inbox mark-unread ` | `mark_conversation_unread` |
| Forward a message | `POST /api/v1/conversations/{id}/messages/{messageId}/forward` | `sequenzy inbox forward --to ` | `forward_conversation_message` |
| Inbox address | `GET /api/v1/conversations/inbox-address` | `sequenzy inbox address` | `get_inbox_address` |
| Branded address | `PUT /api/v1/conversations/inbox-address` | `sequenzy inbox address --local-part --domain ` | `update_inbox_address` |
### List Conversations
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/conversations?status=open&unread=true" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Get Conversation
```bash theme={null}
curl "https://api.sequenzy.com/api/v1/conversations/{id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Reply to Conversation
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/conversations/{id}/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "outbound",
"bodyText": "Thanks for reaching out! Here is the information you requested..."
}'
```
### Configure Reply Tracking
Use the company endpoint to read or update the same settings exposed in the
dashboard:
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/companies/{companyId}" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"replyTrackingEnabled": true,
"replyTrackingDomainMode": "sequenzy",
"forwardReplies": true
}'
```
The CLI exposes these as `sequenzy companies update` flags, and MCP exposes
them on `get_company` and `update_company`. Company reads also return the
read-only `replyRetentionDays` value.
### Read Reply Metrics
The account, campaign, and sequence metrics endpoints return `replies` and
`replyRate`; sequence responses include the same fields for every email step.
The CLI `stats` command renders both fields, and the MCP `get_stats`,
`get_campaign_stats`, and `get_sequence_stats` tools return them unchanged.
## Data Retention
Received emails and their stored attachments are kept for the company's
retention period, at least 90 days, and then deleted. Conversations and their
message text stay in the inbox; attachments of deleted emails remain listed by
name but can no longer be downloaded. Read the current value through
`replyRetentionDays` on the company or `retentionDays` from Get Inbox Address;
it is not editable through the public API, CLI, or MCP server.
Deleted emails and attachments cannot be recovered.
## Limitations
* **Reply tracking requires inbound email routing**: Sequenzy's shared inbound domain works automatically. Branded reply domains require the inbound MX record shown during domain verification.
* **Attachments**: Stored for the retention period when attachment storage is configured. Without it, only file names and sizes are kept
* **One inbox per workspace**: The inbox address is per company, not per agent
* **Rate limits**: High-volume reply handling may be subject to processing delays
## Troubleshooting
### Replies not appearing
1. Verify **Receive email in Inbox** is on in **Settings → Inbox**
2. If using **Your domain**, check that the inbound MX record is verified and inbound routing shows as active
3. Confirm the reply was sent to the correct address (check email headers)
### Forwarding not working
1. Ensure **Auto-forward replies** is enabled
2. Verify the original email had a valid forwarding destination. For direct
transactional API sends, provide `replyTo` or configure a company default
reply profile.
3. Check the forward status in the reply details
### Missing conversation context
If replies appear without conversation context:
* The original email may have been deleted
* The `emailSendId` in the reply address may be invalid
* Check the subscriber exists in your account
# Advanced Sender Routing
Source: https://docs.sequenzy.com/concepts/sender-routing
Use one verified From address while routing mail through another verified sending domain from sender settings
Advanced sender routing lets Sequenzy separate the address your recipient sees from the verified domain Sequenzy uses for routing and dedicated IP assignment.
Example:
* Visible `From`: `hey@pawpulse.ai`
* Routed via: `hey.pawpulse.ai`
This is useful when you want a cleaner root-domain sender in the inbox while still isolating traffic on a verified subdomain.
## Availability
This is available directly in sender profile settings.
Sender profiles can keep a visible `From` address on one verified domain and route mail through another verified sending domain in the same workspace.
## Requirements
* The visible `From` domain must be verified in your workspace
* The routing domain must also be verified in your workspace
* Dedicated IP assignment follows the routing domain, not the visible `From` domain
## What Changes
* Recipients still see your visible `From` address
* Sequenzy chooses the SES configuration set and dedicated IP from the routing domain
* Campaigns, sequences, transactional emails, test sends, and forwarded replies all use the same routing decision
## What Does Not Change
Advanced sender routing selects which verified domain Sequenzy uses for its
managed delivery infrastructure. It does not configure a customer-managed
outbound SMTP relay. External SMTP providers such as Hostinger cannot be
selected as the delivery transport because Sequenzy owns outbound delivery.
You can keep an external mailbox or alias as the visible From address after its
domain is verified, and use it as Reply-To for manual replies.
## In The UI
Create the visible sender identity first. Its From name and address are what
recipients see in their inbox.
### Add A Reply-To Identity
Add a separate Reply-To identity only when replies should go to a different
mailbox than the visible From address.
For most workspaces, sender setup still looks the same.
Sender settings show an extra routing-domain control, and campaign review shows a small `via your-subdomain.com` hint when the visible sender and routing domain differ.
## Several Display Names On One Address
One mailbox can carry more than one sender identity. `hello@mail.example.com`
can send as `Example` for most of your mail and as `Brennon at Example` for a
single sequence, without renaming the shared identity that every other campaign
and sequence uses.
Send `fromEmail` together with `fromName` and Sequenzy picks the identity of
that name on that address, creating it the first time you use it. Matching
ignores case, so a name you have already used is reused rather than duplicated.
To rename an identity everywhere it is used instead, use `update_sender_profile`
or sender settings.
Reply-To works differently: an address carries one Reply-To display name for the
whole workspace. If you send a `replyToName` that does not match the saved
profile for that address, the saved name is kept and the response `warnings`
array tells you so. Sequence email steps are the exception, because a step
stores its own Reply-To header and applies the name you sent to that step only.
A step's Reply-To follows the profile it points at. Editing that profile's
address or name updates every sequence step using it, including steps in
sequences that are already running. A per-step `replyToName` is the one
exception: it stays attached to that step, and the profile still supplies the
address. Send `replyToName: ""` to drop the override and go back to following
the profile's name. Because steps depend on profiles this way, a reply profile
used by a step in an active sequence cannot be deleted until the step points
somewhere else.
## Best Practices
* Keep both the visible `From` domain and routing domain under the same brand
* Use a subdomain for routing when you want stronger reputation isolation
* Move transactional and marketing traffic onto separate routing domains when possible
* Warm up any new routing domain or dedicated IP gradually
## Related Guides
* [Dedicated IPs](/concepts/dedicated-ips)
* [Verify a sending domain](/guides/domain-verification)
* [Use subdomains for email](/guides/subdomain-sending)
# Sequences
Source: https://docs.sequenzy.com/concepts/sequences
Build automated email workflows that respond to subscriber behavior
Sequences are automated email workflows that send the right message at the right time based on subscriber actions, tags, segments, or events. They're the heart of marketing automation in Sequenzy.
## Saving in the sequence editor
The editor always shows whether your sequence is **Draft**, **Active**, or **Paused**, along with any sending or enrollment restrictions. When you have pending edits, use the save controls to save or discard your sequence changes. Email content is saved separately in the email editor.
Step settings stay in your draft until you save the sequence. **Done** closes a step's settings without saving the sequence. Saving step changes updates an active sequence. Email content is saved separately in the email editor.
A/B test actions save immediately: selecting a winner, adding or deleting variants, and starting another test. **Discard** does not undo these actions. Selecting a winner changes what future subscribers receive without waiting for you to save the sequence.
After an A/B test step reaches its test sample size, it picks a winner automatically once one variant leads on the winning metric. The lead is checked as subscribers reach the step. Until then, subscribers keep being split across variants. You can select the winner yourself at any time, before or after that. The step needs at least two variants. To switch to another variant, open the step and choose **Change Winner**. Subscribers who already got a variant keep it; everyone who reaches the step afterwards gets the new winner.
If a winner was picked before you were sure, choose **Resume Testing** and confirm. The winner is cleared and subscribers are split across the variants again, with the results collected so far kept. Resuming turns off **Pick winner automatically** in the step's settings, so the test keeps running until you select a winner; you can turn it back on there. While a resumed test runs, its variants can't be added or removed. To start over with freshly generated variants, use **Run another test** from the step's **More** menu.
Before you activate, **Review setup** lists the remaining blockers. Choose a blocker to open its step, review the canvas, or save your changes. The checklist updates as you fix your draft.
When you add a step, the picker names its destination and highlights the target on the canvas. The palette also shows where clicking a step type will add it; dragging lets you choose another position.
Use the eye button on a linked email step to preview its saved content without leaving the canvas. The preview uses a sample contact and sends nothing. Choose **Edit email** to open the email editor. Save new steps or changed email selections before previewing them.
## What Are Sequences?
A sequence is a series of connected steps that execute automatically:
```
┌─────────────────┐
│ TRIGGER │ ← Starts the sequence
│ (Tag or Event) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ ACTION │ ← Does something
│ (Send Email) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ LOGIC │ ← Controls flow
│ (Wait 3 days) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ ACTION │
│ (Send Email #2) │
└─────────────────┘
```
## Private Welcome Previews
If you receive a private welcome-sequence link, you can review all four emails before creating an account. Claiming the preview creates a new workspace with the saved emails and their design settings as a draft sequence. You then choose the audience and finish setup before activating it.
You can edit the imported emails in your workspace. The original preview link keeps showing the saved preview, so later workspace edits are not shared through that link. Each preview can be claimed by one account.
## Sequence Components
### Start from a Saved Audience
In **Subscribers**, open a saved segment or list and choose **Create sequence** beside its name, or use the action in its menu. The template picker shows the audience you selected. You can choose a template, start blank, or describe the sequence to AI while keeping that trigger selected.
For a segment, the sequence triggers when contacts enter it. For a list, it triggers when contacts join that list. The sequence starts as a draft so you can review its emails and timing before activation.
### Triggers
Triggers define **when** a sequence starts. Each sequence has exactly one trigger.
| Trigger Type | Description | Use Case |
| - | - | - |
| `contact_added` | Contact added to the workspace or to a list | Welcome sequence |
| `tag_added` | Tag applied to subscriber | Customer onboarding |
| `segment_entered` | Subscriber enters segment | Lifecycle automation |
| `segment_exited` | Active subscriber stops matching a segment | Win-back, downgrade |
| `event_received` | Event triggered via API | Post-purchase flow |
| `inbound_webhook` | Event from a connected app such as Cal.com | Booking follow-up |
| `inactivity` | Contact has not triggered an event for N days | Win-back |
| `frequency` | Contact triggers an event N times within Y days | Power-user outreach |
| `manual` | Nobody enters automatically; you enroll an audience | Black Friday countdown |
### Triggering on Several Lists or Tags
A `contact_added` trigger can name more than one list, and a `tag_added` trigger more than one tag. The values are combined with **or**: a contact who joins *any* of the listed lists, or receives *any* of the listed tags, enters the sequence.
To set this up in the dashboard, open the trigger step and choose **Specific lists...** in the **List** dropdown (or pick several tags in the **Tag** picker). Over the API, CLI, and MCP the fields are `listIds` and `tagNames`; each accepts up to 25 values.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sequences" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"trigger": "contact_added",
"listIds": ["list_abc123", "list_def456"],
"goal": "Welcome new subscribers and introduce the product"
}'
```
A contact enters the sequence **once**, no matter how many of the trigger's lists or tags they match. Whichever value fired first is recorded on the enrollment and shown as **Entered via** in the sequence's subscriber list, so you can tell at a glance which list or tag brought each contact in. The same attribution is on `enteredVia` in [list enrollments](/api-reference/sequences/list-enrollments), in `sequenzy sequences enrollments`, and in the `list_sequence_enrollments` MCP tool. Each enrollment also stores redacted **branch decisions** (which if/else path it took, and whether the compared field was missing, empty, nonempty, or equal to the expected graph value). For a completed token recorded before that stamp existed, [get enrollment](/api-reference/sequences/get-enrollment) reconstructs the walk from ClickHouse.
Multi-value triggers keep the normal lifecycle stop automatically. For a
multi-tag trigger, **tag missing** means the tag recorded in **Entered via**;
for a multi-list trigger, **not in list** means the list recorded there. A
contact entering through Trial is therefore checked against Trial, while one
entering through Newsletter is checked against Newsletter. You can still
replace this entry-aware default with a specific tag, list, event, or field
condition.
### Contact-Added Trigger Scope
When you pick the `contact_added` trigger without choosing any list, the **List** dropdown offers two readings of "no specific list":
* **Any contact** (the default) starts the sequence for every contact that is added, whether or not they join a list. Use this when contacts arrive from an integration such as PostHog, Stripe, or Supabase - those contacts join no list unless you configure list targeting, so this is the only scope that reaches them.
* **Any list** starts the sequence only once the contact actually joins a list. Use this when list membership, not the contact existing, is what the sequence is about.
Picking one or more specific lists scopes the trigger to those lists and ignores the setting above. Sequences created before this setting existed behave as **Any contact**, which is what they always did.
**Enroll Existing Subscribers** follows the same rule: an **Any contact** sequence backfills every active contact, an **Any list** sequence backfills only contacts who are in at least one list, and a multi-list or multi-tag sequence backfills everyone matching any of its values.
### Segment-Entered Trigger Behavior
The `segment_entered` trigger is designed to avoid surprise sends.
* When you activate a sequence, Sequenzy uses the current segment members as a baseline and does **not** enroll them automatically.
* Only contacts who enter the segment after that baseline are enrolled.
* Segment triggers are evaluated after relevant subscriber data changes, with a background check at least every 8 minutes as a fallback.
* You can optionally stop active contacts if they later leave the triggering segment.
* If you edit the segment definition or reactivate the sequence later, Sequenzy creates a new baseline instead of backfilling historical matches.
If you want to send the sequence to contacts who are already in the segment, use **Enroll Existing Subscribers** manually.
### Segment-Exited Trigger Behavior
The `segment_exited` trigger starts a sequence when a contact stops matching a
segment, for example leaving "Active customers" for a win-back flow.
* When you activate the sequence, the current members become the baseline. Nobody is enrolled at activation, and contacts who left earlier are not enrolled.
* A contact is enrolled when they stop matching the segment's filters while staying an active contact. Unsubscribing, bouncing, complaining or being deleted does not count as leaving.
* Exits are checked after relevant subscriber data changes, with a background check at least every 8 minutes. A contact who leaves and matches again within seconds, before the check runs, is not enrolled.
* Editing the segment's filters, editing or deleting a segment it references, or switching the trigger between entered and exited creates a new baseline, so contacts who stop matching because of your edit are not enrolled.
* Bulk changes to the data the segment filters on also count as leaving. Deleting a list, removing a tag from everyone or clearing an attribute the segment uses enrolls every member who stops matching, so pause the sequence first. Historical imports and backfilled events never count as leaving.
* To stop the sequence when the contact comes back, add an `entered_segment` stop condition for the same segment.
### Manual Trigger and One-Time Countdowns
A `manual` trigger never enrolls anyone on its own. Use it for one-time,
calendar-anchored campaigns such as a Black Friday countdown: build the steps,
set the sequence's **countdown moments** (its key dates), enable it, then press
**Enroll audience** in the header to add everyone, a list or a segment. The
same run is available as `POST /sequences/:id/enroll-audience`, `sequenzy
sequences enroll-audience`, and the `enroll_sequence_audience` MCP tool. It
walks the audience in the background, skips contacts already in the sequence,
never enrolls unsubscribed or bounced contacts, and shows its progress in the
same header button while it runs.
Two options in that sheet cover the usual countdown timing:
* **Start on a date**: enroll now, or at a moment you pick. The run stays
scheduled (visible in the header) and can be cancelled before it starts.
* **Keep enrolling people who join later**: anyone who joins the chosen list
or segment after the run still enters, landing on the step the countdown is
at (a Saturday joiner skips the teasers and gets the last-day emails). The
sequence stays manual; a background sync re-checks the audience every few
minutes and switches itself off after the last moment, or as soon as you
change the trigger away from manual. Over the API this is
`audienceAutoEnroll` on the sequence.
Some sends need values that are the same for everyone in the run and only
known when you start it: a sale end time, a discount percentage, an order cut-off time.
Paste them as **Run data** in the enroll sheet, or pass `data` to
`POST /sequences/:id/enroll-audience` (`--data-json` in the CLI, `data` in
`enroll_sequence_audience`). Every contact the run enrolls keeps a copy, and
the sequence's emails and conditions read it as `{{enrollment.sale_ends_at}}`;
the editor's variable picker lists the fields of the latest run. A later run
with different data only affects the contacts it enrolls; anyone still active
from an earlier run is skipped and keeps that run's values. Auto-enroll syncs
carry no run data.
Automatic checks only enroll contacts with no previous enrollment in this sequence.
Failed or cancelled enrollments stay stopped; retry them explicitly when needed.
Switching off **Keep enrolling** prevents new automatic runs. A run created before
you switch it off keeps its existing audience; cancel that run separately to stop
its remaining batches.
The sequences list shows a countdown with its next moment ("Counting down to
Black Friday starts · Nov 27"), whether it is live or over, and whether it
keeps enrolling.
The **Black Friday & Cyber Monday Countdown** template in the sequence gallery
sets all of this up: a manual trigger, four key dates pre-filled for the next
season, and seven emails timed from them, with copy generated for each moment
in the countdown. Add **Your offer** in the same dialog (the discount, code,
what it covers and any exclusions); it is stored as the sequence description
and every email quotes it. Without an offer the emails name the sale window
and exact start and end times but never invent an amount or code. You can add
or change the offer later by editing the sequence description or a step's
description, then regenerating the affected emails.
### What does not start a sequence
Only the trigger types above start a sequence. In particular, **updating a subscriber's custom attributes never enrolls anyone**. Attributes describe a contact, so writing one is not something that happened at a point in time and nothing listens for it.
If your system updates an attribute and you want a sequence to run, pick one of these instead:
* **Send an event** for the thing that happened. This is the better option in almost every case: enrollment is immediate, and the event payload stays available to every email in the run as `{{event.*}}` merge tags.
* **Build a segment** on the attribute and use a `segment_entered` or `segment_exited` trigger. This works, but membership is recomputed after relevant changes rather than at the moment you write the attribute, so it is a poor fit when timing matters.
Keeping the attribute is still worthwhile - attributes are what you filter segments on and personalize with. They just are not a starting gun.
### Actions
Choose an action from the step picker. Selecting **Send Email** opens the email
step controls shown below, where you choose content and delivery behavior.
#### Where a step's email comes from
Every Send Email step gets its own email. Describe what the email should say,
then pick where its design starts from:
* **Blank** - AI writes a new email from your brand profile, or start with an
empty canvas.
* **Template gallery** - pick a designed template. It is copied into a new
email for this step, and with a description AI rewrites only the copy inside
it, keeping the layout, images, and links exactly as designed.
* **Your templates** - emails you marked as templates on the Emails page (or in
the editor header, or with `isTemplate` through the API, CLI, and MCP). Use
one master design across every step: each step gets an independent copy, so
editing step 3 never changes step 1 or the master.
* **One of your emails** - any campaign, sequence, or transactional email. It is
copied too, so the original stays untouched.
Choosing **Generate in this design** keeps the structure and rewrites the text
for your description; **Use a copy** duplicates the design as is. If you really
want several steps to share one email, choose **Link instead**: the step then
points at the same email, and edits apply everywhere it is used. Steps whose
email is shared show a **Shared** chip on the canvas, and the step panel offers
**Make an independent copy** to detach it. [Learn more about email
templates](/concepts/email-templates).
Actions perform operations on the subscriber.
| Action Type | Description |
| - | - |
| **Send Email** | Send an email - from a template, a copy of an existing email, or AI |
| **Update Tags** | Add a tag or remove a tag from a subscriber |
| **Update Lists** | Add a subscriber to a list or remove them from a list |
| **Create Discount** | Mint a per-subscriber Stripe or Shopify discount code |
| **Call Webhook** | Call an external API and optionally use its response in later steps |
| **Call AI** | Write a personalized line per contact and insert it in later emails |
When you add an **Update Tags** step in the builder, you choose whether that step should add a tag or remove one. You can change that choice later by editing the step.
When you add an **Update Lists** step, you choose whether the step should add the subscriber to a list or remove them from one. This is useful for routing contacts into new subscription tracks, suppressing them from a list mid-sequence, or chaining list-triggered sequences together.
### Call Webhook Steps
A **Call Webhook** step sends an HTTP request to any HTTPS endpoint when a subscriber reaches it - and, unlike a fire-and-forget ping, it can save the response for the rest of the sequence. This makes fresh, at-send-time data available to your emails: look up the current account state, pricing, usage, or any other data from your own API right before an email goes out.
* **Request**: choose `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`, plus optional headers and a JSON body template. The URL, header values, and body all support merge tags such as `{{email}}`, `{{subscriber.plan}}`, and `{{event.order_id}}` (use `{{urlencode.email}}` in query strings). In the JSON body, merge tags must sit inside quotes (`{"email": "{{email}}"}`) so the template is valid JSON as written - values are then escaped for you, and quotes or newlines in the data cannot break the request. A tag in a bare value position (`{"amount": {{total}}}`) is not accepted; quote it instead. In the builder the step keeps what you typed and shows the problem until you fix it, and the sequence cannot be activated while it is unresolved; over the API, CLI, and MCP the write is rejected outright. If you leave the body empty on a `POST`-style request, Sequenzy sends a default payload with the subscriber and sequence context. If a merge tag in the URL *path* has no value for a subscriber, the request is not sent - `https://api.example.com/accounts/{{event.account_id}}` would otherwise collapse to `https://api.example.com/accounts/` and hit the wrong resource - and the step follows your failure behavior instead. Empty query-string tags are sent as-is.
* **Save the response**: set a result key (for example `lookup`) and the JSON response is stored for that enrollment. Later email steps can then use merge tags like `{{webhooks.lookup.data.status}}` or `{{webhooks.lookup.status}}`. Nested fields and array indexes work: `{{webhooks.lookup.data.items.0.title}}`. In the email editor, the variable picker (type `{{`) offers these tags for every webhook step that runs before the email - and once the step has run at least once, it suggests the actual field names seen in recent responses, with sample values. The most recent response also prefills the email preview, so `{{webhooks...}}` tags render with real data instead of placeholders.
* **Failure behavior**: pick what happens when the endpoint fails or times out (10 seconds) - continue to the next step, exit the sequence, or stop the enrollment with an error. When a failed call is set to continue, `{{webhooks.KEY.ok}}` is `false` and `{{webhooks.KEY.status}}` holds the HTTP status (`0` when the request never completed). The response body is still saved, so `data` merge tags can resolve to whatever the endpoint returned with the error - wrap them in `{{#if webhooks.KEY.ok}}...{{/if}}` before putting those values in an email. Only a request that never completed, such as a timeout, leaves `data` empty.
* **Seeing what happened**: the step's `N completed` badge in the builder lists each subscriber with the HTTP status and the host and path that was called, and a failed call that was set to continue or exit is flagged there in red - so a `500` never looks the same as a clean `200`. Click that line to open the full response: the status, the endpoint, the error if there was one, and the saved JSON body, which you can copy. A test run shows the same status line for the request it really sent. The query string is left out of the displayed URL, since it can carry merge-tag values. The body is only available when the step has a result key - without one it is never read from the response at all.
A common pattern for "look up details right before sending": trigger the sequence from an event, delay until close to the moment that matters, call your API for the current values, then send the email that references `{{webhooks.KEY.data...}}` values.
Webhook URLs must use HTTPS and cannot point to private network addresses. Merge tags belong in the path and query - not in the hostname, credentials, or port, and not inside `{{#if}}` or `{{#each}}` blocks - so that subscriber data can never change which server the request reaches. If a merge tag in the URL *path* resolves to nothing, or to a value that walks the path up (`../`), the request is not sent and the step follows your failure behavior. Responses are stored per enrollment (up to 32 KB per result).
### Call AI Steps
A **Call AI** step (called **Ask AI** in earlier versions of the builder) writes a short piece of text for each subscriber when they reach it, and stores it for the rest of the sequence. It is not an email writer: you keep writing the email, and the step fills the one part that has to be different for every contact. You describe what that line should say in a prompt, define the named fields the AI must return, and later emails insert them with `{{ai.KEY.field}}` merge tags.
For example, in a churn-save sequence you can pass the cancellation event and the contact's attributes, and ask for a single sentence that names why they left, how long they had been a customer, and the one thing worth offering them. The email around it stays the same for everyone; that sentence does not.
* **Prompt**: written like a merge-tag template, so `{{first_name}}`, `{{event.reason}}`, and even `{{webhooks.KEY.data.field}}` values from an earlier webhook step resolve per contact before the prompt is sent. When you add a fresh Call AI step, the builder pre-fills a draft prompt, output fields, and context selection from what the sequence already knows - its trigger, the trigger event's data shape, and the attribute keys your contacts carry. Review the draft, regenerate it, or start blank; the "Draft for me" button reruns the same drafting at any time. You can cancel a draft or rewrite and keep your current text.
* **Context**: you choose exactly which contact data is shared with the model - the trigger event data, the contact's recent activity (their last 1-50 events, newest first), specific custom attributes, and their tags. Nothing you do not select is sent. Over the API, CLI, and MCP the same selectors are `includeEventProperties`, `includeRecentEvents` with `recentEventLimit`, `includeAttributes`, and `includeTags`.
* **Output fields**: the model must return the named fields you define (for example `churn_reason_line` and `winback_angle`), each a short fragment capped at a maximum length, with an optional fallback. Output is structured only: free-form model text never lands in an email. Give every field a fallback so a rare model failure degrades to your copy instead of a broken email.
* **Test**: the editor keeps a Test panel beside every section, so you can edit the prompt, output, or contact data and run it again without switching screens. Press `Cmd+Enter` (`Ctrl+Enter` on Windows and Linux) anywhere in the editor to run. A run can cover up to three contacts at once, so you see whether the line holds for different people - recent people who went through the sequence are offered first (their stored trigger event data makes the test realistic), or search for any contact. Nothing is sent to them. Earlier runs stay listed under the newest one, marked with what you changed since, and **Restore this version** puts the prompt, output fields, and contact data back to what an earlier run used. Your last six runs are kept while the editor is open. Tests use the exact same generation path the live sequence uses. A step is usually built before the sequence has ever run, so when the contact has no stored trigger event data the test falls back to the most recent event of that name in your account, and then to the example payload shown in the builder's Event Payload panel. You can also point the test at any other event your workspace knows, to see how the line reads for that payload. Each result says which of the three sources it used, and you can rerun a real-data test against the example values.
* **Tuning**: when a tested line is not quite right, choose **Not quite right? Tell AI what to change** and say what should be different ("never mention the price", "sound like a friend, not a brand") and the step's prompt is rewritten to hold for every future contact. The rewrite is an edit, not a restart: it keeps the merge tags and the rest of your wording, lands in the Prompt section where you can read and adjust it, runs the test again automatically, and is rejected if the feedback would turn the step into an email writer or strip out the per-contact data.
* **Failure behavior**: by default a failed generation continues with the fallback text. You can instead exit the sequence or stop the enrollment with an error.
Call AI steps are free while the step is in beta. You will be told before
anything is billed.
In the email editor, the variable picker (type `{{`) offers `{{ai.KEY.field}}` tags for every Call AI step that runs before the email. Results are stored per enrollment, and each step keeps its own result key, so duplicating a step never overwrites another step's output.
### Discount Codes In Emails
A **Create Discount** step mints a fresh code for each subscriber who reaches it, using your connected Stripe or Shopify account. Because the code is generated per subscriber, you reference it with merge tags rather than typing it into the email:
| Merge tag | Renders as |
| - | - |
| `{{discount.code}}` | The generated code, for example `WINBACK-8F2A` |
| `{{discount.value}}` | Pre-formatted offer, for example `20% off` |
| `{{discount.percentOff}}` | `20` |
| `{{discount.amountOff}}` | Amount in the smallest currency unit, e.g. `1000` |
| `{{discount.currency}}` | `usd` |
| `{{date.discount.expiresAt}}` | Expiry formatted for the recipient's locale |
These resolve for any recipient who has passed the discount step earlier in their run. The cleanest way to present the code is a **Discount Code** block, which is pre-filled with `{{discount.code}}`, but the tag also works inline in any text block.
If a step can be reached by branches that don't all pass through the discount
step, the tags stay empty for the recipients who took a branch that skipped
it, and only for those. Preview an email with [Render Sequence Step
HTML](/api-reference/sequences/render) to check the copy: it fills the tags
with a placeholder `TEST-CODE` and reports anything it could not resolve. A
preview has no way to know which branch a recipient took, so it only fills the
tags when every path into the step runs the same discount step - and in the
branchy case it lists them as `no_value` rather than as unrecognized names,
because they do resolve for the recipients who took the discount path.
### Logic Nodes
Logic nodes control the flow of the sequence.
| Logic Type | Description |
| - | - |
| **Delay** | Wait a fixed time, until a date from the event, until a weekday window, or until a key date |
| **Condition** | Continue if a condition matches; otherwise, exit the sequence |
| **Branch** | Multi-path branching (if/else if/else), or a weighted random split |
| **Wait for Event** | Pause until an event occurs or timeout |
### Delay: fixed duration or "wait until a date"
Choose how a Delay step waits under **Wait** in the step editor:
* **For a fixed time** (default): wait a set number of days, hours, and minutes
before continuing.
* **Until a date**: resume on a date that belongs to each contact. Read it from
the **trigger event** (for example `renews_at` or `booking.startsAt` in the
event payload) or from a **contact attribute** (for example `dueDate` or
`renewalDate`). The value must be an ISO date such as `2027-02-15` or a Unix
timestamp.
In "until a date" mode you can add an optional offset to resume before or after
the date, for example **1 day before** a renewal or **2 hours after** a
booking. If the date is missing or cannot be read, the step either continues to
the next step or exits the sequence, depending on the fallback you choose.
Event dates only exist in event-triggered sequences. Attribute dates work with
any trigger, including a list, tag, segment, or manual enrollment, and are read
when the contact reaches the step. If the attribute moves later while a contact
is waiting, for example a corrected due date, the contact keeps waiting until
the new date. Moving it earlier does not release a waiting contact sooner. If
the attribute is removed while a contact waits, the missing-date fallback
applies.
Each "until a date" step also decides what happens when the date **already
passed** when a contact reaches it:
* **Continue to the next step** (default): the contact moves on immediately.
* **Skip ahead to the next wait**: the contact skips every email and action
until the next wait, condition or branch, so they only receive what is still
ahead of them.
* **Exit the sequence**: the contact leaves at this step.
This lets contacts join a date-based timeline at any point. For a pregnancy
sequence with emails 20, 12 and 4 weeks before a `dueDate` attribute and one
2 weeks after it, set the first three waits to skip ahead and the last one to
continue. Someone who joins 5 months before the due date receives every email
from the 20-week one on. Someone who joins after the birth only receives the
last one.
Over the API, CLI and MCP the step field is `waitUntil` (`{ field, source,
days, hours, minutes, direction, missingAction, pastAction }`), where `source`
is `event` or `attribute`.
### Delay: "wait until a weekday window"
A Delay step can also hold contacts until the next occurrence of a weekday
inside a local-time window, for example next Sunday between 09:00 and 12:00 in
`America/Los_Angeles`. Contacts that reach the step while the window is open
continue immediately; everyone else waits for the next occurrence - a Friday
enrollment waits until Sunday morning, and a Sunday-afternoon enrollment waits
a week.
Place this step immediately before the email when the send day matters, such as
a weekly digest sequence that must only ever send on Sundays, including when
contacts hand off from another sequence mid-week. The step controls when the
flow resumes, so any step you put between the wait and the email (another
delay, a wait-for-event) shifts the send off the window again. If the platform
ever misses the window (a rare operational failure), the step rolls to the next
configured day rather than resuming late.
Configure it in the dashboard by opening a Delay step and setting **Wait** to
"Until a weekday", then picking the days, the local-time window, and the
timezone. The same step is available through the
[API](/api-reference/sequences/create), [CLI](/concepts/cli), and
[MCP](/concepts/mcp) surfaces.
### Delay: "wait until a key date"
Key dates are named moments stored on the sequence itself, for example "Black
Friday starts" or "Cyber Monday ends", edited under **Countdown** (K) in the
builder toolbar and listed on the canvas as milestones. A Delay step set to
**Until a key date** waits relative to one of them: **7 days before Black
Friday starts**, **1 hour before Black Friday ends**, or exactly **at Cyber
Monday starts**. The step card shows the moment as a calendar badge with the
resolved time, and the Countdown dialog previews when every email goes out for
someone enrolled today.
Because the dates live in one place, moving a key date re-times every step
that waits for it, including contacts already parked on that step. A date
moved earlier pulls their wake-up in; a date moved later pushes it out; a date
moved into the past wakes them now.
Each key-date step also decides what happens to contacts who reach it **after
the moment already passed**, which is what makes late enrollment safe:
* **Skip ahead to the next wait** (default): the contact skips every step
until the next wait, so someone enrolled on Saturday only receives the
"last day", "final hour" and Cyber Monday emails, never the "7 days to go"
teaser.
* **Send right away**: the contact receives the next step immediately.
* **Exit the sequence**: the contact leaves at this step.
Activation is blocked while a step references a key date that no longer
exists. Over the API, CLI and MCP the sequence field is `keyDates` and the step
field is `waitUntilKeyDate` (`{ key, days, hours, minutes, direction,
pastAction }`).
### Branch: condition split or random split
A Branch step routes each subscriber down one of several paths. It has two
modes:
* **Condition split** (default): each path carries a condition such as
`has_tag`, `in_segment`, or `link_clicked`. Paths are evaluated in order and
an else path catches everyone who matches nothing.
* **Random split**: each path is a weighted variant. When a subscriber reaches
the step they are assigned a path by percentage, so nothing about the
subscriber is evaluated. This is how you run a concurrent A/B test inside a
sequence - for example sending half of your abandoned-cart contacts a
discount offer and half a free-shipping offer at the same time, instead of
running two duplicate sequences one after another and comparing across
different weeks.
A random split has no else path, because every subscriber lands on one of the
weighted paths. The percentages must sum to 100, and the paths carry no
conditions.
In the dashboard sequence builder, add **Percentage Split**, choose 2–5
variants, and set their traffic allocations. The builder keeps the row at 100%
for you: when you change one variant, the others move in proportion, and a
stacked bar shows the resulting split. To hold a variant steady while you dial
in the rest - say 30/20/50 - lock it with the padlock beside its percentage, and
only the unlocked variants will absorb your next change. **Split evenly** resets
the row and clears every lock. You can edit the labels and
percentages later by opening the split step. The same split is available
through the [API](/api-reference/sequences/update), [CLI](/concepts/cli), or
[MCP](/concepts/mcp) by passing `splitMode` and `randomPercentages` on a branch:
```bash theme={null}
sequenzy sequences update seq_123 \
--branch-json '{"afterNodeId":"node_cart_wait","splitMode":"random","randomPercentages":[50,50],"branches":[{"id":"discount","label":"10% off","steps":[{"subject":"Here is 10% off","html":"
Complete your order.
"}]},{"id":"bonus","label":"Free shipping","steps":[{"subject":"Free shipping on us","html":"
Complete your order.
"}]}]}' \
--confirm-structural-change
```
To test two versions of a **single email step** and have a winner picked for
you automatically, use an [A/B test](/api-reference/ab-tests/create) on that
step instead (`automationNodeId`). A random split is the right tool when the
two variants differ by more than the email itself - different delays,
different follow-ups, or a different number of steps on each path.
### Sending Windows
Sequences can have an optional local-time sending window, such as weekdays from
08:00 to 20:00. A trigger can still enroll the subscriber immediately, but every
email action waits until the next allowed local time before sending. This applies
to the first email and later emails after delays, branches, or wait steps.
A sending window shifts each email's send time; it does not reschedule delays
or other steps. When the calendar day of a send must be guaranteed at the graph
level, combine it with a "wait until a weekday window" Delay step before the
email.
#### Realigning contacts after you change the window
Changing the window on a live sequence deliberately leaves contacts who are
already waiting on an email-bound delay step alone: they keep the wake-up time
their delay produced. So widening a Sunday window from 09:00 to 08:00 does not
move the people already queued for 11:13 next Sunday, and narrowing it to end
before 11:13 would defer them to the *following* Sunday instead.
Realignment fixes this without cancelling or re-enrolling anyone. It pulls each
waiting contact back to the window's opening on the day they were already
scheduled for. A wait only ever moves earlier, never onto a different local day,
and never before now - so nobody changes weekly cycle and nothing is sent
retroactively. Per-step "wait until a weekday" gates are respected, and running
it twice does nothing. A sequence sending window never advances a delay whose
next action is SMS, a webhook, a branch, or another non-email step.
Preview it first, then apply:
```bash theme={null}
sequenzy sequences realign-enrollments seq_abc123
sequenzy sequences realign-enrollments seq_abc123 --apply
sequenzy sequences realign-enrollments seq_abc123 --job-id
```
The same operation is available through the
[API](/api-reference/sequences/realign-enrollments) and the
`realign_sequence_enrollments` [MCP](/concepts/mcp) tool. Applied requests run
as background jobs; poll the returned job ID before continuing with any
`nextCursor` from the completed result.
### Frequency Cap
A frequency cap keeps a contact from getting too many marketing emails when
several sequences and campaigns overlap. Set the rule once for your workspace
in **Settings → Tracking** (Frequency Cap section), for example at most 1 email every 16 hours or
3 emails every 7 days. Then turn on **Respect frequency cap** in the settings of
each sequence that should follow it. Sequences that don't opt in are never
capped.
When an email step comes up for a contact who already received the cap's number
of marketing emails in the window, the step is skipped and the contact moves
straight on to the next step. The skip shows on their activity timeline.
Campaign emails and marketing emails from any sequence count, including earlier
emails from the same sequence, so a cap of 1 per 16 hours also spaces out a
sequence whose steps are closer together. Campaigns always send; they count
toward the cap but aren't held back by it. Transactional emails never count and
transactional steps are never skipped.
When two capped sequences reach an email for the same contact at the same
moment, one sends and the other waits a few seconds, then checks the cap again.
A campaign email sent in that same moment can still put the contact one email
over the cap. Leave the cap off for time-sensitive sequences, such as cart
recovery or password flows, where every email must go out.
Manage the rule with the [Frequency Cap API](/api-reference/frequency-cap/get),
`sequenzy frequency-cap` in the CLI, or the `get_frequency_cap`,
`set_frequency_cap` and `clear_frequency_cap` MCP tools. Opt a sequence in with
`frequencyCapEnabled` on [Create Sequence](/api-reference/sequences/create) and
[Update Sequence](/api-reference/sequences/update), `--respect-frequency-cap`
and `--ignore-frequency-cap` in the CLI, or `frequencyCapEnabled` in the MCP
sequence tools.
### BCC Copies
You can set one or more optional BCC addresses in a sequence's settings
(separate multiple addresses with commas, up to 10). Every email the sequence
sends then delivers a hidden copy to those addresses, which is useful when a
team inbox (like customer support) needs visibility into what subscribers
receive. BCC recipients are invisible to the subscriber. Leave the field empty
to turn it off.
### File Attachments
Email steps can carry real file attachments - useful for lead magnets, guides,
or any file the subscriber expects after triggering the sequence. Attachments
are URL-backed: you provide a public HTTPS URL plus a filename, and Sequenzy
fetches the file at send time and attaches it to the email. This keeps the
sequence config small and lets you update the file at its source without
editing the sequence.
In the dashboard, open an email step, click the settings (gear) icon in the
top-right corner of the editor, and use the **Attachments** section to upload
a file - it's stored on Sequenzy's file hosting and attached from there at
send time. Choose **From trigger** when every enrollment has a different file,
such as an eTicket or invoice. The URL and filename can use values from the
event that enrolled the subscriber:
```json theme={null}
[
{
"filename": "file-{{event.order_id}}.pdf",
"path": "{{event.file_url}}"
}
]
```
The same shape works through the API, CLI, and MCP tools. Static public HTTPS
URLs remain supported, and an empty array removes all attachments. Dynamic
attachment values are resolved from the enrollment's original event snapshot,
then the final filename and public URL are validated before the file is fetched.
If a required event value is missing or invalid, the step fails without sending
an attachment-less email.
Limits: up to 10 attachments per email and 15MB total per email (after
fetching). If the URL stops resolving, the send is retried and then recorded
as a failed step, so keep attachment URLs stable while a sequence is enabled.
Signed URLs must remain valid until the step sends and its fetch retries finish.
For deliverability, prefer a hosted download link (which is also click-tracked)
when the file is large or the audience is broad; use a real attachment when
recipients expect the file directly in their inbox, such as a lead-magnet
delivery.
## Building Sequences
### The Visual Editor
Sequences are built in a visual drag-and-drop editor:
1. **Add a trigger** - How does the sequence start?
2. **Add actions** - What should happen?
3. **Add logic** - When and how should it happen?
4. **Connect nodes** - Define the flow between steps
5. **Activate** - Turn on the sequence
Already have subscribers who match your trigger? Use [Enroll Existing
Subscribers](/concepts/enroll-existing-subscribers) to add them retroactively.
### Editor Shortcuts and Tools
The editor supports fast, keyboard-friendly workflows:
* **Undo / redo** - Cmd+Z undoes any unsaved canvas edit (add, edit, delete,
reorder, and even Discard changes); Cmd+Shift+Z redoes it. History clears
once changes are saved.
* **Step finder** - Cmd+F opens a search across every step by name or
subject and jumps the canvas to the selected one.
* **Arrow navigation** - Arrow keys walk between steps (left/right move
across branch paths). Enter opens the focused email step, Delete removes
the focused step, Escape clears the focus.
* **Duplicate step** - Every action and delay step has a Duplicate button in
its editor. Duplicating an email step also clones the email itself, so you
can save the sequence and then edit the copy without touching the original.
* **Collapse branches** - Wide branch sections collapse into a compact
summary card (hover the branch step for the toggle). The hidden steps stay
part of the sequence; this only tidies up the canvas.
* **View toggles** - M toggles per-step metrics, F toggles the funnel view,
G opens goals, T opens a test run, and Comma opens settings.
### Deleting Steps While People Are In Them
You can delete an otherwise-deletable action, delay, or logic step while
subscribers are inside it. Anyone currently in the deleted step moves to the
next step immediately and continues through the sequence - the same path new
enrollees take after the edit. If the deleted step was the last one, those
subscribers complete the sequence. The editor shows a confirmation with the
number of affected people before the delete, and the canvas displays a notice
if any subscribers ended up stuck on a step that was removed in the past.
Trigger and end steps cannot be deleted. A/B test steps also remain protected
while variants are generating or while a test is running or has selected a
winner.
Recipient migration is refused when people are inside a deleted branch step
and multiple outgoing paths survive. There is no single next step to move them
to, so reconnect the graph to one continuation first or wait for them to leave
the step.
### Simulate A Subscriber's Path
Use **Simulate path** in the toolbar to dry-run a subscriber through the
sequence without sending anything. Sequenzy evaluates every condition and
branch with the same logic the send path uses, based on the subscriber's
current tags, lists, segments, and attributes, then highlights the resulting
path on the canvas with a breakdown of each decision.
Conditions that depend on what happens during the run - like "clicked a link
in this sequence" or event-payload fields - cannot be known ahead of time.
The simulation flags these as assumptions and follows the fallback path, the
same default the live run would take if the activity never happened.
Wait-for-event steps are shown as pass-through steps in the preview. The
preview does not predict whether their event will arrive or whether they will
time out; use a live test when that timing-dependent outcome matters.
The same dry run is available from the
[API](/api-reference/sequences/simulate), `sequenzy sequences simulate`, and
the `simulate_sequence` MCP tool. Without a subscriber those surfaces report
who currently matches and whether the sequence is ready. Nobody is
auto-enrolled when you activate.
### Funnel View
On an active or paused sequence, press F (or use the funnel toggle in the
toolbar) to see where the sequence leaks. The funnel only annotates what
matters:
* **Entry volume** - one chip at the top shows how many subscribers ever
entered the sequence.
* **Losses** - a connection only gets a chip when subscribers were actually
lost between the two steps, shown as `-N` with the continuation
percentage. Hover the chip for the breakdown: how many failed, stopped
early (stop condition or manual removal), or finished the sequence there.
Subscribers still waiting in a delay are not counted as lost.
* **Branch splits** - fan-out connections show how traffic distributes
across the paths.
* **Engagement decay** - each email step shows its open-rate change versus
the previous email (for example `opens -4.2pts vs prev`). In a linear
sequence almost everyone reaches the next step, so this is usually the
real drop-off signal: it tells you which email to rewrite.
Connections where nothing was lost stay unannotated, so anything you see in
funnel view is worth looking at. Combine it with the metrics view (M) to see
absolute opens, clicks, and conversions per email at the same time.
### Create From A Dashboard Prompt
You can also start a sequence directly from the dashboard landing page or onboarding flow with a prompt.
* Try prompts like `Create a welcome sequence` or `Build a re-engagement sequence`
* During onboarding, the assistant also suggests starter questions like `What do I start with?` and `How can I add more about my company?`
* You can also ask review questions like `Show my top performing sequence` to jump from a performance check into the sequence itself
* If Sequenzy recognizes the intent, it opens a matching starter template in the builder
* If your request is more custom, Sequenzy creates a blank draft sequence and takes you straight into editing
* If you keep the conversation going, the assistant opens a follow-up chat so you can ask for more detail without repeating the earlier request
### Run A Live Sequence Test
Use **Run sequence test** in the sequence builder when you want to check the whole workflow with one real subscriber before activating or changing a live sequence.
Sequence test runs execute the same graph that production uses:
* The run starts from the sequence trigger and follows connected steps in order
* Branches, conditions, event-property merge tags, discounts, tags, lists, subscriber attributes, and webhooks use the selected subscriber's real data
* Delay and wait steps are compressed with the selected speed multiplier, such as 60x, 120x, 240x, or 480x
* Sequence emails are sent through the test-email path with a `[TEST]` subject prefix
* Recent test runs show step-by-step logs, accelerated wait timing, sent-email references, and failures
Non-email actions run for real on the selected subscriber. Use a safe internal
or dedicated test subscriber when checking flows that update tags, lists,
attributes, discounts, or webhooks.
Before starting a run:
1. Save any pending changes in the builder.
2. Choose one existing active subscriber. Direct email recipients are not available for live sequence tests because the run needs subscriber state.
3. Pick a speed multiplier. `120x` turns a 1-hour wait into about 30 seconds.
4. Confirm the run. Sequenzy prevents another queued or running test for the same sequence and subscriber.
Test emails are excluded from the normal sequence sent-email filters, but the run log keeps references to the email send records created during the test.
### Restrict A Live Trigger To An Allowlist
A sequence test starts from the trigger node, so it does not tell you whether
your real event or segment trigger actually fires and matches. When you want the
production trigger to run for real but only a chosen set of contacts to receive
the sequence - a first pass on the live trigger, a phased rollout, or a pilot
group - use a negative stop condition as an allowlist.
Stop conditions are re-evaluated before every step, including the first one. So
`does_not_have_tag` cancels the run before anything sends for anyone who does
not carry the tag:
1. Tag the contacts you want to receive the sequence, for example `allowlist`.
2. Set the sequence stop condition to `does_not_have_tag` with value `allowlist`.
3. Activate the sequence and fire the real trigger.
Everyone the trigger picks up is cancelled at the trigger node except your
tagged contacts. `removed_from_list` works the same way against a list instead
of a tag. Clear the stop condition, or replace it with the real lifecycle stop,
before you open the sequence to everyone.
```bash theme={null}
sequenzy sequences update seq_123 \
--stop-condition-json '{"type":"does_not_have_tag","value":"allowlist"}'
```
Guarded-out contacts still enroll and are then cancelled at the trigger node,
so no step runs for them. They show up as cancellations on the trigger node,
not in the active or waiting enrollment counts, and the funnel entry chip
(which counts contacts that reached the first step) does not include them.
Clearing the guard does not retry the contacts it already cancelled. Nothing
re-enrolls them on its own, so they only receive the sequence if the trigger
fires for them again - which one-shot triggers such as a purchase event or
`contact_added` never do. On the `one_time` enrollment mode it is permanent
even then: enrollment is blocked by the existence of any earlier run, not by
its outcome, so a repeated trigger is ignored too. Guard a duplicate sequence
and leave the real one ungated, or enroll the missed contacts afterwards with
`sequenzy sequences enroll`, which skips the `one_time` check.
A sequence has one stop condition, so while the guard is in place you cannot
also use a stop condition for its normal lifecycle purpose. For that reason this
works best on a duplicate of the sequence rather than the live one.
### Stop On A Later Event With Property Filters
`event_received` stop conditions only match events received after enrollment.
The enrolling event itself, and any earlier history, never satisfies the stop -
so a sequence can safely trigger on an event and stop on the same event name.
To stop only when a later event carries specific property values, add a
`matchConfig` with `event_property_filter` mode. The filters use the same shape
and operators as event trigger `propertyFilters` (`exists`, `not_exists`,
`equals`, `not_equals`, `one_of`, `contains`, `greater_than`, `less_than`), and
the stop fires only when one event received after enrollment matches every
filter.
For example, a quota-nudge sequence that enrolls when a user consumes their
first credit (`quota_used = 1`) and should stop as soon as they consume another
one:
```bash theme={null}
sequenzy sequences update seq_123 \
--stop-condition-json '{
"type": "event_received",
"value": "product.credit_consumed",
"matchConfig": {
"mode": "event_property_filter",
"propertyFilters": [
{ "path": "quota_used", "operator": "greater_than", "value": 1 }
]
}
}'
```
Three other `matchConfig` modes are available:
* `event_property` (for `event_received` with an event-based trigger): stop
only when the stop event's field equals the same field captured on the
enrolling event, for example the same `orderId`:
`{"mode":"event_property","rules":[{"entryFieldPath":"orderId","eventFieldPath":"orderId"}]}`
* `field_value` (for `field_changed`): stop only when the field changes to a
matching value:
`{"mode":"field_value","operator":"equals","value":"pro"}`
* `entry_audience` (for tag/list defaults): use the tag or list recorded
when each contact enrolled. Public responses use `value: null` with
`{"mode":"entry_audience","audience":"tag"}` (or `list`). This is the
same representation stored by the sequence. Tag entry matching requires a
`tag_added` trigger; list entry matching requires a `contact_added` trigger
scoped to at least one specific list.
In the dashboard, open the trigger step and use the **Only stop when
properties match** toggle in the stop-condition section to build the same
filters visually.
### When A Stop Condition Actually Cancels
A stop condition is re-evaluated **when an enrollment next runs a step**, not
when its event arrives. Sending the stop event does not wake a waiting
enrollment.
So for a contact parked on a three-day delay:
1. The stop event arrives. It is recorded, sync rules apply, and any sequence
triggered by it enrols - but the waiting enrollment is untouched.
2. The enrollment keeps reporting `waiting` for the rest of the delay.
3. When the delay expires and the enrollment wakes, the stop condition is
checked **before** the next step runs, and the enrollment is cancelled.
Nothing sends.
The outcome is correct - no step ever runs once the condition holds - but the
enrollment status does not change until the delay is up. A test that fires a
stop event and immediately re-lists enrollments will always still see
`waiting`, which looks like the stop condition failed.
This is the opposite of a **wait for event** step, which *is* resumed the
moment its event arrives. A step-level wait reacts to the event; a
sequence-level stop condition is checked on the next step.
To confirm a stop event registered without waiting the delay out, ask for the
current match:
```bash theme={null}
sequenzy sequences enrollments seq_123 \
--subscriber-id sub_123 --stop-condition-match
```
`stopConditionMatches: true` means the condition matches at the time of the
read. The worker re-checks before a future step, but this annotation is not a
cancellation lock: the condition can change, and a step already past its stop
check may still finish. The same annotation appears on the subscriber's page in
the dashboard, in [list sequence
enrollments](/api-reference/sequences/list-enrollments) via
`stopConditionMatch=true`, and in the `list_sequence_enrollments` MCP tool. A
`null` value means the match was not determined, never "does not match". The
listing's `stopCondition.matchConfig` shows any event-property filters, field
comparison, or per-enrollment entry audience that also has to match.
If you need the enrollment to *read* as cancelled straight away rather than at
its next step - a lifecycle test, or an external system that polls enrollment
status - cancel it directly instead:
```bash theme={null}
sequenzy sequences cancel-enrollments seq_123 --subscriber-id sub_123 --apply
```
When a stop condition seems not to fire at all, check these before anything
else:
* **A sequence has exactly one stop condition.** A stop event with any other
name never applies, however plausible it looks. The enrollment listing echoes
the configured one.
* **Only events received after enrollment count.** Backdated events (more than
an hour in the past) are recorded as history and never satisfy a stop.
* **A `matchConfig` adds payload requirements.** With `event_property` or
`event_property_filter`, the stop event must carry the matching fields, not
just the right name. Duplicating a sequence copies its `matchConfig` too, so
a test copy inherits whatever the live sequence required.
### Example: Welcome Sequence
```
┌─────────────────────┐
│ Trigger: Tag Added │
│ "new-signup" │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Send: Welcome Email │
│ "Getting Started" │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Delay: 2 days │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Send: Tips Email │
│ "5 Pro Tips" │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Delay: 3 days │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Send: Feature Email │
│ "Discover Features" │
└─────────────────────┘
```
### Example: Post-Purchase Sequence
```
┌────────────────────────┐
│ Trigger: Event │
│ "saas.purchase" │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Send: Thank You Email │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Delay: 7 days │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ Wait for Event: │
│ "feature_activated" │
│ Timeout: 14 days │
└───────────┬────────────┘
│
┌────┴────┐
│ │
Event Fired Timeout
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ Add Tag │ │ Send Help │
│ "engaged" │ │ Email │
└───────────┘ └───────────┘
```
## Personalizing Sequence Emails With Event Properties
When a sequence starts from an event trigger, Sequenzy stores that event's properties on the sequence run. You can use those values in any later email in the same sequence with `event.` merge tags.
Examples:
* `{{event.city}}`
* `{{event.windMaxSpeed}}`
* `{{event.alert.maxSpeed}}`
This is useful when the event contains context that should stay stable across multiple emails, such as a weather alert region, the purchased plan, or the trial length that started the flow.
### How It Works
* The event snapshot is stored on the automation run when the subscriber enters the sequence
* Every later email in that same sequence reads from the same stored snapshot
* A later event for the same subscriber does not overwrite earlier sequence runs
That means your second or third email still references the original event data that enrolled the subscriber.
### Example
If your app sends:
```json theme={null}
{
"event": "weather.wind_alert",
"properties": {
"city": "Tel Aviv",
"windMaxSpeed": 75,
"duration": {
"hours": 6
}
}
}
```
You can write:
* Subject: `Wind alert for {{event.city}}`
* Body: `Winds may reach {{event.windMaxSpeed}} km/h for {{event.duration.hours}} hours.`
### Defaults
You can use fallback values if a property may be missing:
* `{{event.city|your area}}`
* `{{event.duration.hours|a few}}`
### Using Event Properties in Update Subscriber Steps
The same merge tags work in **Update Subscriber** step values, so a sequence can copy event data onto the subscriber record:
* Set a `plan` attribute to `{{event.plan}}`
* Set an `mrr` number attribute to `{{event.amount}}` (the resolved value is stored as a number)
* Set first name to `{{event.firstName}}`
In the dashboard, type `{{` in a value field to autocomplete properties from the trigger event's recent payloads, plus subscriber attributes and built-in fields. If a referenced property is missing from the event and the tag has no fallback, that field is skipped rather than being set to an empty value.
API, CLI, and MCP callers can create the same step explicitly:
```json theme={null}
{
"type": "update_subscriber",
"nodeType": "action_update_attributes",
"config": {
"firstName": "{{event.firstName}}",
"customAttributeUpdates": [
{ "name": "plan", "value": "{{event.plan}}", "valueType": "text" },
{ "name": "mrr", "value": "{{event.amount}}", "valueType": "number" },
{ "name": "active", "value": "{{event.active}}", "valueType": "boolean" }
]
}
}
```
Number and boolean values must be literals or one standalone merge tag. Use `subscriberUpdateSteps` with the node ID to replace an existing Update Subscriber step's config through the API, CLI, or MCP.
## Enrollment Modes
Enrollment mode controls whether the same subscriber can enter a sequence again. It defaults to `unlimited`, so a tag-triggered drip sends again if the tag is ever re-applied - set `one_time` unless repeat runs are what you want. Back-in-stock and replenishment sequences are the exception: they default to `matching_field` so each product is tracked separately.
You can change the mode at any time, on a draft or a live sequence: in the dashboard under the sequence's enrollment settings, or by passing `enrollmentMode` to [Update Sequence](/api-reference/sequences/update), `sequenzy sequences update`, or the `update_sequence` MCP tool. Changing it affects future enrollments only; runs already in flight continue.
| Mode | Behavior |
| - | - |
| `unlimited` | A subscriber can re-enter after a previous run finishes, but cannot have duplicate active runs. |
| `one_time` | A subscriber can enter the sequence once ever. |
| `matching_field` | Event-triggered sequences can have one active run per matching event field value. |
`matching_field` is only available when the sequence starts from an event. Use it when the same subscriber may legitimately have multiple active runs, but each run belongs to a different event-scoped object.
Examples:
* `order.id` - one active run per order
* `product.providerVariantId` - one active run per product variant
* `subscription.id` - one active run per subscription
In the dashboard, the **Matching field path** input suggests fields from recent payloads for the selected trigger event. Through the API, CLI, and MCP, set `enrollmentMode` to `matching_field` and pass `enrollmentFieldPath`.
The path must resolve to one scalar value. Array traversal such as `lineItems[].providerProductId` is not supported for enrollment keys; use `propertyFilters` to filter on array items, then choose a scalar order, product, variant, or subscription identifier for `enrollmentFieldPath`.
```json theme={null}
{
"name": "Post-purchase follow-up",
"trigger": "event_received",
"eventName": "ecommerce.order_placed",
"enrollmentMode": "matching_field",
"enrollmentFieldPath": "order.id",
"goal": "Follow up about this specific order"
}
```
For Shopify back-in-stock and replenishment sequences, you can leave `enrollmentFieldPath` empty. Sequenzy uses built-in product and variant matching defaults for those events.
### Unsubscribed and Bounced Contacts
Unsubscribed and bounced contacts still enroll when a trigger matches. This keeps sequences useful beyond marketing email: tag changes, attribute updates, webhooks, and emails marked as transactional (for example an order-shipped notification) all still run for them. Marketing email steps are skipped automatically for these contacts.
If a contact unsubscribes while already moving through a sequence, the remaining enrollment is cancelled instead - the sequence's own emails were the most likely cause, so continuing would only produce more skipped sends.
## Commerce Sequence Stop Behavior
Commerce event triggers get product-aware defaults so subscribers do not keep receiving product sequences after the sequence is no longer relevant.
### Replenishment Due
Sequences triggered by `ecommerce.replenishment_due` default to stopping when the same product is purchased again.
Sequenzy matches purchases by:
* `provider`
* `providerProductId`
* `providerVariantId` when the replenishment setting uses variant-level matching
* `lineItems[].providerProductId` and `lineItems[].providerVariantId` on later `ecommerce.order_placed` events
This means a replenishment sequence for one product does not stop just because the subscriber bought a different product.
### Back In Stock
Sequences triggered by `ecommerce.back_in_stock` default to stopping when either of these happens:
* The subscriber buys the same item
* The same item goes out of stock again
Back-in-stock matching checks:
* `provider`
* `providerProductId` or `product.providerId`
* `providerVariantId` or `product.providerVariantId`
* `lineItems[].providerProductId` and `lineItems[].providerVariantId` on later purchase events
* `ecommerce.back_in_stock_out_of_stock` events for the same product and variant
If you trigger these commerce events manually, send the same product fields that Shopify and WooCommerce send. See [Events](/concepts/events#commerce-product-matching) for the expected shape.
## Condition Types
Conditions let you control the sequence based on subscriber data:
* **If** continues only when the condition matches. Otherwise, the subscriber exits the sequence.
* **If/Else** takes the first matching path, or the **Else** path if none match.
### Tag Conditions
```
Condition: has_tag "customer"
├─ Yes → Send upsell email
└─ No → Send conversion email
```
### List Conditions
```
Condition: in_list "VIP Customers"
├─ Yes → Send exclusive offer
└─ No → Send standard offer
```
### Segment Conditions
```
Condition: in_segment "Project invite recipients"
├─ Yes → Send project onboarding
└─ No → Send standard onboarding
```
### Event Conditions
```
Condition: event_received "project.invite.accepted"
├─ Yes → Start accepted-invite flow
└─ No → Send reminder
```
Event checks can be scoped with `activityScope`: `this_sequence` checks events
received since the contact entered this sequence, `previous_email` checks events
tied to the last sequence email, and `ever` checks the contact's full event
history.
### Clicked Link Conditions
```
Condition: link_clicked "project-invites"
├─ Yes → Start project setup flow
└─ No → Send invite reminder
```
Leave the URL empty to match any tracked click, or enter part of the URL to
match a specific link. Click checks support the same `activityScope` values:
`this_sequence`, `previous_email`, and `ever`.
### Field Conditions
```
Condition: field_equals "plan" "enterprise"
├─ Yes → Assign to sales team
└─ No → Send self-serve resources
```
### Available Operators
| Operator | Description | Example |
| - | - | - |
| `has_tag` | Subscriber has tag | `has_tag "customer"` |
| `does_not_have_tag` | Subscriber does not have tag | `does_not_have_tag "customer"` |
| `in_list` | Subscriber in list | `in_list "Newsletter"` |
| `in_segment` | Subscriber matches a saved segment | `in_segment "Project invite recipients"` |
| `event_received` | Event was received | `event_received "project.invite.accepted" scope=this_sequence` |
| `link_clicked` | Link was clicked | `link_clicked "project-invites" scope=previous_email` |
| `field_equals` | Field exact match | `field_equals "plan" "pro"` |
| `field_contains` | Field contains string | `field_contains "company" "Inc"` |
| `field_greater_than` | Numeric comparison | `field_greater_than "mrr" 100` |
| `field_less_than` | Numeric comparison | `field_less_than "age" 30` |
| `has_phone` | Subscriber has a phone number | `has_phone` |
| `sms_subscribed` | Subscriber opted in to SMS | `sms_subscribed` |
## Wait for Event
The "Wait for Event" node pauses execution until a named event is recorded for the contact.
### What you can wait for
Choose any built-in or custom event name:
| Event name | Waits for |
| - | - |
| `email.opened` | The contact opens an email |
| `email.clicked` | The contact clicks a link in an email |
| `email.replied` | The contact replies to an email |
| `ecommerce.order_placed` | The contact places an order |
| Any custom event | The event you send through the API |
The node matches on the event **name** only. Unlike a trigger, it cannot be narrowed with property filters, so any occurrence of that event name resolves the wait - a contact waiting on `ecommerce.order_placed` after buying one product is released by an order for any other product too.
If you need the run to be about one specific product, order, or variant, scope the whole sequence with [`matching_field` enrollment](/api-reference/sequences/create) rather than trying to narrow the wait. That gives each contact a separate run per event field value, so each run's wait is already about the right object.
### Timeout behavior
Every wait has a timeout measured in days. A wait has a **single outgoing path** - the timeout does not create a second branch. When the timeout expires, the contact takes one of two actions you choose:
| Timeout action | Result |
| - | - |
| **Continue** | The contact moves to the next step, the same step the event leads to |
| **Exit** | The run ends and the contact leaves the sequence |
Because "continue" reuses the same path, a step placed after the wait runs both for contacts whose event arrived and for contacts who timed out. To treat those two groups differently, put a Condition after the wait:
```
┌──────────────────────┐
│ Wait for Event: │
│ "demo_scheduled" │
│ Timeout: 5 days │
│ On timeout: continue │
└──────────┬───────────┘
│ event received OR timed out
▼
┌──────────────────────┐
│ Condition: │
│ event_received │
│ "demo_scheduled" │
└──────────┬───────────┘
┌────┴────┐
Yes No
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Send │ │ Send │
│ Prep │ │ Follow │
│ Email │ │ Up Email │
└──────────┘ └──────────┘
```
## Branching
### Simple Condition (2 paths)
```
┌─────────────────────┐
│ Condition: │
│ has_tag "customer" │
└──────────┬──────────┘
│
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Upsell │ │ Convert │
│ Email │ │ Email │
└──────────┘ └──────────┘
```
### Multi-Branch (3+ paths)
```
┌─────────────────────────────┐
│ Branch: │
│ ├─ plan = "enterprise" │
│ ├─ plan = "pro" │
│ └─ else (default) │
└─────────────┬───────────────┘
│
┌────────┼────────┐
│ │ │
Enterprise Pro Default
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ VIP │ │ Pro │ │ Basic │
│ Email │ │ Email │ │ Email │
└────────┘ └────────┘ └────────┘
```
### Empty paths
A path does not have to contain a step. Subscribers who take an empty path pass
straight through to whatever comes after the branch, so you can save and
activate a sequence with empty paths. The editor shows where each empty path
leads and highlights empty paths while the sequence is live. When an empty path
continues into a step that only it feeds, you can also remove the empty path
tile without changing where its subscribers go.
## Sequence States
| State | Description |
| - | - |
| `draft` | Being edited, not active |
| `active` | Running, processing subscribers |
| `paused` | Holding existing subscribers and blocking new entry |
| `archived` | No longer in use |
Paused sequences do not execute nodes. Subscribers already in the sequence stay
at their current step, and new subscribers who match the trigger are not
enrolled while the sequence is paused. When the sequence resumes, due
subscribers continue gradually from the step where they were held.
You can also stop new enrollments without pausing the sequence. In that mode,
current subscribers keep moving until completion, but new trigger matches are
not enrolled. In the sequence editor, both options are in the **Pause** menu,
and the status shows **Enrollment closed** until you choose **Open
enrollment**.
Waits are wall-clock based. If a subscriber is waiting three days and the wait
expires while the sequence is paused, the subscriber becomes due but does not
continue until the sequence resumes.
### Sending Paused
Company sending pauses and real account bans are parent locks, not
manual sequence pauses. An active sequence remains active, still captures new
entrants, and keeps tokens at their current step while email sending is blocked.
The dashboard shows this as **Sending paused** so you can distinguish it from a
sequence you paused yourself.
When sending is restored, due sequence work wakes automatically. If you do not
want a specific sequence to continue after sending is restored, pause that
sequence manually; the manual pause remains until you resume it.
## Tokens (Subscriber Journeys)
When a subscriber enters a sequence, a "token" tracks their progress:
```json theme={null}
{
"subscriberId": "sub_abc123",
"automationId": "auto_xyz",
"currentNode": "node_456",
"status": "active",
"context": {
"entryPoint": "tag_added",
"emailsSent": 2,
"waitingFor": "email_opened"
}
}
```
### Token States
| State | Description |
| - | - |
| `active` | Currently progressing through sequence |
| `waiting` | Held at a wait/delay or wait-for-event node |
| `completed` | Finished all steps |
| `failed` | Error occurred |
| `cancelled` | Manually stopped |
## Real-World Sequence Examples
### SaaS Trial to Paid
```
Trigger: Event "saas.trial_started"
│
▼
┌───────────────────┐
│ Send: Trial │
│ Welcome Email │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Wait: 3 days │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Send: Tips Email │
│ "Get the Most..." │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Wait: 4 days │
└─────────┬─────────┘
│
▼
┌───────────────────────┐
│ Condition: has_tag │
│ "customer" │
└───────────┬───────────┘
│
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
(End) ┌───────────────┐
│ Send: Last │
│ Chance Email │
└───────────────┘
```
### E-commerce Abandoned Cart
```
Trigger: Event "ecommerce.cart_started"
Continue while: Tag "ecommerce.in_cart"
│
▼
┌───────────────────┐
│ Wait: 1 hour │
└─────────┬─────────┘
│
▼
┌───────────────┐
│ Send: Cart │
│ Reminder │
└───────┬───────┘
│
▼
┌───────────────┐
│ Wait: 1 day │
└───────┬───────┘
│
▼
┌───────────────┐
│ Send: 10% Off │
│ Coupon │
└───────────────┘
```
### Re-engagement Campaign
```
Trigger: Tag Added "inactive-30d"
│
▼
┌───────────────────┐
│ Send: We Miss You │
└─────────┬─────────┘
│
▼
┌─────────────────────┐
│ Wait for Event: │
│ "session_started" │
│ Timeout: 7 days │
└─────────┬───────────┘
│
┌────┴────┐
│ │
Event Timeout
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Remove │ │ Send │
│ Tag │ │ Special │
│ inactive │ │ Offer │
└──────────┘ └────┬─────┘
│
▼
┌──────────┐
│ Wait │
│ 14 days │
└────┬─────┘
│
▼
┌──────────┐
│ Add Tag │
│ "churned"│
└──────────┘
```
## Best Practices
### 1. Start Simple
Begin with 3-5 step sequences before building complex flows:
```
Trigger → Email → Delay → Email → End
```
### 2. Use Tags for State
Track sequence progress with tags:
```
- "onboarding-started" (at trigger)
- "onboarding-complete" (at end)
```
### 3. Set Reasonable Delays
* Welcome emails: immediate to 30 minutes
* Follow-ups: 2-4 days
* Re-engagement: 1-2 weeks
### 4. Always Have Exit Conditions
Check if the goal is achieved before sending more emails:
```
Condition: has_tag "customer"
├─ Yes → End sequence
└─ No → Continue emails
```
### 5. Personalize with Conditions
Different paths for different subscribers:
```
Branch on plan:
├─ Enterprise → High-touch content
├─ Pro → Feature-focused content
└─ Free → Conversion content
```
## Test with event properties
In **Test sequence**, choose **Live test** and enter optional **Test event properties** as a JSON object, for example `{"cart_id":"cart_123","event_ids":["event_1","event_2"]}`. Your steps can use `{{event.cart_id}}`; arrays and nested values keep their types. Leave the field blank for no event data. This does not record a trigger event, but live tests perform real sequence actions for the selected subscriber. Emails are marked as tests. Closing the dialog clears the input. [Start and inspect live tests through the API](/api-reference/sequences/test-run), CLI or MCP as well.
### Reuse received event data for sequence tests
Use `sequenzy events sample --company --json`, MCP `get_event_sample`, or `GET /api/v1/events/sample?eventName=` to read the latest retained payload in your workspace. Requires `subscribers:read`. This includes older and custom events across all subscribers; `sample: null` means none exists. Copy `sample.properties` into the test run's `customVariables` (CLI `--event-properties`) and edit as needed. The dashboard's **Use latest received event** button replaces the JSON field. Copying data does not change the selected recipient or fire an event. Clear the field, omit the properties, or pass `{}` to test without overrides. Lookup errors are safe to retry; starting a real test still has delivery side effects.
You can schedule an empty audience or enable **Keep enrolling people who join
later** before any contacts are eligible. Incremental checks also include
contacts who become active later, such as after double opt-in confirmation.
Failed or cancelled checks keep the last successful scan boundary for recovery;
completed enrollments are skipped when those contacts are checked again.
If a countdown date edit cannot queue its retiming work, the edit is rolled back.
Retry the same edit once the service recovers.
## Related
Use tags to trigger sequences
Trigger sequences with events
One-time broadcasts vs sequences
Immediate emails via API
# Skills
Source: https://docs.sequenzy.com/concepts/skills
Install Sequenzy as a reusable skill for AI agents
You can install Sequenzy as a reusable skill for AI agents that support the `skills` ecosystem. This gives your agent a versioned workflow guide for operating Sequenzy safely instead of relying on ad hoc prompting.
## Install
Install the Sequenzy email marketing skill with:
```bash theme={null}
npx skills add Sequenzy/skills --skill sequenzy-email-marketing
```
## What The Skill Covers
The `sequenzy-email-marketing` skill helps your agent:
* authenticate and check identity and session state
* manage subscribers, lists, tags, and segments
* create and edit campaigns, sequences, and templates
* control campaigns (cancel, pause, resume, delete, duplicate) and run A/B tests
* enroll subscribers into sequences
* invite team members
* triage and reply to inbox conversations
* manage outbound webhooks
* generate draft email content
* send transactional email and read delivery stats
* recognize when a workflow is not supported yet and should move to the dashboard or direct API use
The repository also ships a general `sequenzy` skill for broader product
questions; prefer `sequenzy-email-marketing` for day-to-day email work.
## Why Use A Skill
Without a skill, your agent has to infer Sequenzy workflows from scratch each time. With a skill, you give it:
* a stable command reference
* use-case driven guidance
* explicit caveats for unsupported or partial CLI flows
* a versioned source of truth you can update as Sequenzy evolves
## Repository
The public source lives here:
* [Sequenzy Skills on GitHub](https://github.com/Sequenzy/skills)
## Install Notes
* After installing, restart your AI tool if it does not pick up the new skill immediately
* Pair it with MCP or the CLI when you want the agent to both understand Sequenzy workflows and execute them directly
# SMS & MMS
Source: https://docs.sequenzy.com/concepts/sms
Send SMS and MMS messages from sequences with the SMS add-on: toll-free number provisioning, consent, quiet hours, cost-weighted credits, international destinations, and click tracking
The SMS add-on lets you send text and picture messages to subscribers in the
United States, Canada, and 20+ international destinations as steps in your
sequences. It is a paid add-on at \$16/month, which includes 1,000 message
credits every month; larger prepaid bundles lower the per-credit price as
volume grows.
SMS sends require either a paid plan (from \$19/mo) or a positive SMS credit
balance. Free workspaces without SMS credits see an upgrade prompt in
**Settings → SMS**; sending pauses automatically when a workspace has neither a
paid plan nor credits.
## Enabling The Add-on
Go to **Settings → SMS** and subscribe to the add-on. If you are upgrading
your plan, you can also add SMS in the same step: the plan upgrade dialog
lists it under **Add-ons** with a volume slider, so you can pick a monthly
credit bundle (from 1,000 credits at \$16/mo up to 500,000) and buy it together
with the plan. SMS is billed monthly even alongside an annual plan. Once
payment completes, Sequenzy automatically:
1. Purchases a dedicated toll-free number for your workspace (no number picker
needed - it just works)
2. Opens the toll-free verification wizard so you can register the number with
carriers
You never deal with a telecom provider directly - Sequenzy manages the number
and carrier relationships for you.
### Adding more numbers
Workspaces can hold up to 100 toll-free numbers, added from **Settings → SMS**.
Each number is verified with the carrier separately and can carry its own
label (shown in campaign sender pickers) and its own brand prefix. Campaigns
pick their sending number in the Message step; sequences, one-off sends, and
standalone test sends use your default (oldest verified) number.
A number stuck in carrier review never blocks adding another, so a second
brand can start its registration while the first is still being reviewed.
### One business, several brands
If you trade under more than one name - say a design studio and a financial
services arm under the same LLC - give each brand its own number. The carrier
registers each number as its own messaging program, so the brand customers
opted in to has to match the number they hear from.
Your **legal business details** (registered name, EIN, address, entity type)
are stored once for the workspace and reused by every number. Each number then
carries its **own brand**: the DBA name, that brand's website, its verification
contact, its consent flow and screenshot, its sample messages, and its use
case. Nothing is inherited from another number, because registering a brand
against a sister brand's website is a rejection.
## Toll-Free Verification
US and Canadian carriers require every toll-free number to be verified before
it can send marketing messages. The verification form asks for:
* Your legal business name, address, and registration number (shared by every
number in the workspace)
* The brand or DBA name this number sends under, and that brand's website
* A contact the carrier can reach about this brand
* A description of how subscribers opt in to SMS, with proof
* Sample messages you plan to send
* An estimated monthly volume
You enter the business details yourself, in **Settings → SMS**: open a number
and choose **Verify this number** (or **Fix and resubmit** if the carrier asked
for changes). Your workspace name and the mailing address in your email footer
are deliberately **not** copied in: carriers check what you submit against your
official business registration, a workspace name is usually a brand, and a
footer address may legally be a PO box or mail drop - both are standard
rejections. Only your website is prefilled.
Fields marked with `*` are what the carrier requires, including your legal
entity type and a screenshot of your opt-in flow. The rest - opt-in keywords
and your privacy and terms URLs - are optional and only improve your odds of a
fast approval, so they never block a submission.
**Save** keeps your details without contacting the carrier, so you can stop and
come back when you have the EIN or the opt-in screenshot. It saves both halves:
the legal business details for the workspace and the brand details for this
number. A resubmission after a rejection starts from what that number last
sent, and the verification page has its own URL so you can send it to whoever
has the missing answer.
The most common rejection is **"Business Name Must Match Official Records"**:
the business name must be your registered legal entity exactly as it appears on
your EIN or state registration - not a DBA or brand name - and the address must
be the one on that registration.
If your messages go out under a brand that is not your legal name, put it in
**Brand / DBA name** and point **Brand website** at that brand's site. Carriers
check the name people opted in to against the registration, so a brand that
appears in your messages but nowhere in the application is the other half of
that rejection - and a brand whose website belongs to a different business is
the same problem again. Sequenzy warns you when the prefix on your sample
message matches neither name.
Review typically takes **1-5 business days**. The SMS settings tab shows a
setup checklist. Every number shows its own state - **Action needed** (you
still have to submit the form), **In carrier review**, **Verified**, or
**Changes required** with the carrier's reason - so one number can be sending
while another is still under review. If a submission is rejected, open that
number and resubmit.
Until the number is verified, the composer and settings are fully available,
but sending is blocked.
## The Number Detail Page
Opening a number from **Settings → SMS** takes you to that number's own detail
page, with everything scoped to it:
* **Status** - Action needed, In carrier review, Verified, or Changes
required, matching the state shown in the settings list
* **Submitted verification data** - the toll-free verification form you
submitted for this number (business details, brand/DBA name, opt-in
description, sample messages, estimated volume), shown read-only.
Visible to workspace admins only
* **Usage** - sends, delivered, failed, credits spent, and the timestamp of
the last message sent from this specific number, so you can see what a
number actually costs and how healthy its delivery is on its own
* **Label and brand prefix** - edit the number's display label and its
per-number brand prefix override from the same page
* **Test send** - send a test message from this specific number (see [Test
Sends](#test-sends))
### Removing A Number
A number's detail page (and its row menu in the SMS settings list) has a
**Remove number** action. Removing a number:
* Releases it back to the carrier. **This is irreversible**, the
number cannot be reclaimed once released
* Frees its slot under the workspace's **100-number cap**, so you can
provision a replacement
* Causes campaigns, sequence steps, and automations still pointed at that
number to **skip their SMS sends** until you repoint them at another
verified number - an explicit "send from" choice is never silently swapped
for a different number. Sends without a specific number keep using your
workspace default (oldest verified) number
The most common reason to remove a number is a **rejected verification** you
don't want to keep resubmitting - for example the brand it was registered
under is being retired, or the details submitted can't be corrected (wrong
legal entity, unfixable business-name mismatch). Remove the rejected number
and purchase a new one to start a clean verification instead of resubmitting
against a number the carrier has already flagged.
Removing a number is available from the dashboard, the [Remove SMS Number
API](/api-reference/sms/delete-number), the `sequenzy sms release` CLI
command, and the `release_sms_number` MCP tool.
## SMS Consent
SMS consent is **completely separate from email status**. A subscriber can be
active for email but not subscribed to SMS, and vice versa.
Every subscriber has:
* A **phone** field (stored in E.164 format, country derived automatically)
* An **SMS status**: `not_subscribed` (default), `subscribed`, or
`unsubscribed`
* A consent **source** and timestamp when subscribed: `import`, `api`,
`manual`, `form`, `checkout`, or `shopify`
Key rules:
* **Consent is never inferred from a phone number.** Adding a phone via the
UI, CSV import, or API does not subscribe anyone to SMS - you must set
consent explicitly, and only when you have express written consent.
* **STOP applies per phone number.** When someone replies STOP, every
subscriber in your workspace who shares that phone number is unsubscribed
from SMS, and the number is suppressed from future sends.
* **START re-subscribes.** If a subscriber who opted out via STOP replies
START, the suppression is removed and they are subscribed again.
You can set consent when importing a CSV (map a phone column and confirm the
contacts gave SMS marketing consent), through the
[subscriber API](/api-reference/subscribers/create) with the `smsConsent`
field, or manually on a subscriber's profile.
### Shopify SMS consent sync
If you use the [Shopify integration](/integrations/shopify), phone numbers and
SMS marketing consent your store collects (for example at checkout) sync
automatically:
* The **initial customer sync** imports each customer's phone number and SMS
marketing state. Customers who opted in at your store arrive as `subscribed`
with consent source `shopify` and Shopify's consent timestamp.
* **Customer webhooks** keep consent current: when a customer opts in or out
on Shopify, the change is applied to the matching subscriber within seconds.
* Consent stays TCPA-safe: a customer is only subscribed on an explicit
Shopify opt-in, an explicit opt-out unsubscribes them, and a phone number
without consent is stored as `not_subscribed`.
Phone numbers are protected customer data on Shopify, so the store must have
approved phone field access. If it hasn't, the sync still runs - it just skips
phone and consent.
## Sending Rules
The **Send SMS** sequence step sends only when all of these are true:
* Your workspace is on a paid plan or has SMS credits available
* The add-on is enabled and your number is verified
* The subscriber has a phone number in a supported country (see
[International destinations](#international-destinations))
* The subscriber's SMS status is `subscribed`
* The phone number is not suppressed (for example by a prior STOP)
If any check fails, the step is skipped with a recorded reason and the
subscriber continues through the rest of the sequence - a missing phone number
never stalls a journey.
You can change what happens to contacts who can't receive SMS (no phone
number, no consent, suppressed, or an unsupported destination) per step: keep
the default **Skip this step** to continue the journey, or choose **Exit the
sequence** to end it for that contact. Workspace-level issues (add-on
disabled, no paid plan or credits, no active number) always skip so an account
misconfiguration never ends journeys in bulk.
To route contacts by SMS reachability instead, use the **Has phone number**
and **Subscribed to SMS** conditions in if/else and multi-branch steps, or the
`Phone Number` and `SMS Status` fields in segments.
### Quiet Hours
For US and Canadian numbers, the window is evaluated in **the recipient's own
local timezone**, inferred from the phone number's area code.
**US numbers** are delivered between **8:00am and 8:00pm local time**, every
day. This keeps every send inside the TCPA window (8:00am to 9:00pm local) and
the stricter Florida and Washington 8:00pm cutoff, while letting sends start as
early as legally allowed instead of waiting for a single nationwide window.
Texas numbers open at **noon local time on Sundays** (Texas Sunday law).
**Canadian numbers** follow the CRTC's Unsolicited Telecommunications Rules
instead, since the TCPA does not apply there: **9:00am to 8:00pm local time on
weekdays**, and **10:00am to 6:00pm local time on Saturdays and Sundays**.
Area codes that straddle two timezones are evaluated conservatively so
recipients on either side stay inside their legal window.
When the recipient's local timezone can't be determined from the area code,
or for destinations outside the US and Canada, messages fall back to a
conservative window of **11:00am to 8:00pm** in the destination country's
reference timezone (Eastern Time for the US and Canada, London time for the
UK, and so on), opening at **2:00pm on Sundays**.
If a send fires outside the window, the message is **deferred until the
window opens** - it is never dropped.
### Message Format
Every message is sent with your brand name as a prefix (for example
`Acme: Your order shipped!`) so recipients always know who is texting them.
The very first message a subscriber receives also includes a compliance
footer: `Msg&data rates may apply. STOP to end, HELP for help.`
## The Composer
The Send SMS step opens a lightweight composer designed for texting:
* **Text** with merge tags for personalization (like `{{firstName}}`) - type
`{{` to pick a merge tag inline
* **Emoji** via an inline searcher - type `:` and a few letters (like
`:fire`) to search and insert an emoji without leaving the keyboard
* **Images** - up to 2 per message, JPEG/PNG/GIF, 1MB max each. Attaching an
image turns the message into an MMS
* A **live counter** showing character count, encoding, segment count, and the
credit cost of the message
* A phone-frame **preview** showing the message exactly as recipients will see
it, including the brand prefix, footer, and shortened links
The composer keeps message length, encoding, segment cost, and the recipient preview visible while you write.
### Segments And Encoding
SMS messages are billed in segments, and the segment size depends on the
characters you use:
| Encoding | Single message | Per segment when concatenated |
| - | - | - |
| GSM-7 (standard characters) | 160 characters | 153 characters |
| Unicode (emoji, non-Latin scripts) | 70 characters | 67 characters |
A single emoji switches the whole message to Unicode encoding, so a 150
character message with one emoji costs 3 segments instead of 1. The composer's
counter shows the encoding and segment count live so there are no surprises.
Sequenzy automatically normalizes typographic characters (smart quotes,
dashes, ellipses) to their standard equivalents so pasted text doesn't
accidentally trigger Unicode pricing.
**MMS** (any message with an image) costs a flat **3 credits** regardless of
text length.
## International Destinations
Sending is not limited to the US and Canada: the UK, Ireland, most of Western
and Northern Europe, Poland, Czechia, Ukraine, Australia, and New Zealand are
supported out of the box. A few things to know:
* **Credits are cost-weighted by country.** A US or Canadian SMS segment is
always 1 credit; international segments consume a per-country multiplier
that reflects what carriers in that country charge (for example a UK
segment costs several credits, and Ukraine more still).
* \***\*Settings → SMS** includes a calculator.\*\* Pick a destination country and
a monthly budget to see how many messages it buys, the effective per-SMS
rate, and the credit multiplier. A "View all country rates" table lists
every supported destination.
* **International marketing is one-way.** Outside the US and Canada, messages
are delivered from a sender that can't receive replies, so keyword STOP is
unavailable; opt-outs run through unsubscribe links instead.
* **MMS is US/Canada only.** Steps with images are skipped for international
recipients with an `mms_not_supported` reason.
* **Unsupported countries are blocked, never silently billed.** If a
subscriber's phone country has no published rate, the step is skipped with
an `unsupported_country` reason and the subscriber continues the sequence.
* Rates are estimates and international carrier costs can change; some routes
may require sender approval before going live.
The subscriber profile shows each contact's phone country, the credit
multiplier that applies to them, and whether they can currently receive SMS
(with the exact reason when they can't).
## Credits And Billing
SMS is prepaid:
* The add-on costs **\$16/month and includes 1,000 credits** each month, and
the per-credit price scales down gradually with larger monthly bundles
* Included credits **renew at the start of each billing period and don't roll
over** - your balance resets to your bundle amount on renewal. This applies
to everything on the balance, including credits granted by support
* Need more credits? Upgrade your monthly bundle any time with the **Upgrade
bundle** button next to your balance on the **SMS** page - your billing
cycle restarts that day, the full new bundle price is charged, and your
balance resets to the new bundle amount
* **1 credit = 1 US/CA SMS segment**; US/CA MMS = 3 credits; international
segments use the per-country multiplier shown in the rate table
* When your balance runs low, you receive an email alert so you can upgrade
before sends start skipping
* If a message can't be sent because your balance is empty, the step is
skipped with a `no_credits` reason and the subscriber continues the
sequence. Sending resumes once credits are available again
Your balance and a transaction ledger are visible in **Settings → SMS**.
## SMS Campaigns
With the add-on enabled, the **Campaigns** screen gains a channel switch at
the top so you can move between email and SMS campaigns. An SMS campaign uses
the same familiar steps as an email campaign - message, recipients, schedule,
review - with a phone-style preview alongside every step.
* **Message**: write the text and, if your workspace has more than one active
number, pick which number the campaign sends from. The default is your
workspace's original number; you can tag each number with a label (for
example "Marketing" or "Support") and give it its own brand prefix override
in **Settings → SMS** so each number keeps its own sender voice. A test sent
from the campaign composer uses the campaign's current selection; sequences,
one-off sends, and standalone test sends use the default number
* **Recipients**: you pick lists, segments, or custom filters exactly like an
email campaign. Sequenzy then narrows the selection to contacts who can
actually receive SMS - subscribed to SMS marketing, phone number on file,
and not suppressed - and shows both numbers so nothing is surprising at
send time. Email and SMS consent are tracked separately, so a contact who
unsubscribed from email still receives your texts if they opted in to SMS
* **Schedule**: send immediately or at a scheduled time. Quiet hours are
always respected per recipient: messages that would land outside the
delivery window are held until it opens, never dropped
* **Review**: shows the SMS-ready recipient count, an estimated credit cost
based on your message's segments, and the final message preview including
the opt-out footer first-time recipients receive
* **While sending**: delivery is throttled to a steady carrier-safe rate. The
campaign page shows live progress and you can stop the campaign at any
point - unsent messages are skipped and marked accordingly
* **Stats**: sent, delivered, clicked (via [short links](#short-links-and-click-tracking)),
failed, skipped, and credits used, per campaign and on the campaign list
Before sending, the review step makes SMS eligibility and cost explicit: it separates contacts who can receive the message from those who will be skipped, then shows the final schedule and estimated credits.
Every recipient re-passes the full [sending rules](#sending-rules) at send
time, so contacts who opt out or run into a suppression after you press send
are skipped safely. If your credit balance runs out mid-campaign, remaining
messages are skipped with a `no_credits` reason and show up in the campaign's
skipped count.
SMS campaigns are created and managed from the dashboard; the API, CLI, and MCP
do not create them. They still appear in the campaign list and detail
reads, where a `type` of `sms` tells them apart - their `subject` and `blocks`
are always empty because they have no email behind them. The email campaign
endpoints (update, schedule, pause, resume, duplicate, resend to non-openers)
reject SMS campaigns with a clear error rather than running them through the
email pipeline. Campaign stats and campaign events reject them too, because
both read email engagement and would otherwise report a fully zeroed send for a
campaign whose texts were delivered - read SMS campaign stats in the dashboard
instead.
## One-Off Sends
You can text a single contact directly from their subscriber page: when the
contact is subscribed to SMS marketing, a **Send SMS** button appears next to
**Send email**. It opens the same composer used in sequences, shows the
estimated credit cost for the contact's destination country, and queues the
message immediately. One-off sends follow all the [sending
rules](#sending-rules) above, including quiet hours - a message composed
outside the delivery window is deferred until it opens.
## Test Sends
You can send a test message to any phone number from the composer, the SMS
settings tab, or a specific number's detail page. Test sends:
* Are limited to **100 per company in a rolling 24-hour window**
* Are **exempt from quiet hours** so you can test any time
* **Charge credits** like a real send
* Are **excluded from step stats**
If your workspace has more than one active number, you can pick which one the
test sends from. Sending from a number's own detail page defaults to that
number; the composer and the general SMS settings tab default to your
workspace's default (oldest verified) number, with a picker if you have more
than one active number.
Your number must be verified before test sends work.
## Short Links And Click Tracking
Links in your messages are automatically shortened to
`https://sqzy.io/...` (about 24 characters). When a recipient taps
a link, the click is recorded and feeds into the same
[revenue attribution](/concepts/metrics) as email clicks, so SMS-driven
purchases show up in your reporting.
## Replies
When a subscriber texts your number:
* **STOP** (and variants) - handled automatically: the carrier-grade
auto-responder confirms the opt-out, and Sequenzy unsubscribes every
subscriber with that phone number
* **HELP** - answered automatically with your help message
* **Anything else** - forwarded to your account email as a notification, so
you never miss a reply
There is no two-way SMS inbox yet - replies arrive as email notifications.
## Step Stats
Each Send SMS step shows its own stats in the sequence builder: sent,
delivered, clicked, opted out, and credits spent. Test sends are excluded.
## Sent SMS Page
The **Sent SMS** tab (sidebar, **Activity → Sent Emails & SMS**) lists every text your
workspace has sent with its delivery status, segment count, and credits
charged. Search by phone number or contact email, filter by status or
campaign, and open any message to see the full body, delivery timeline,
failure reason, and exact credit charge. Header totals sum the messages and
credits for whatever filter you have applied, which makes it the fastest way
to audit what a campaign actually cost.
SMS activity also appears on each subscriber's timeline (sends, deliveries,
failures, clicks, and opt-outs), alongside their email history.
## AI Assistant
The sequence builder's AI assistant can add and edit SMS steps natively. Ask
it things like "add a shipping confirmation text after the order email" and it
proposes an `action_sms` step with drafted copy. SMS additions always require
your explicit approval before they are applied, because they text real
subscribers and spend credits.
## API, CLI, And MCP
SMS is a first-class citizen on every agent surface:
* **API** - create sequences with SMS steps (`type: "sms"` with `text` in
[Create Sequence](/api-reference/sequences/create) steps, branch paths, and
`insertSteps`), edit existing SMS steps with `smsSteps` in
[Update Sequence](/api-reference/sequences/update), draft copy with
[Generate SMS](/api-reference/generate/sms), check readiness with
[Get SMS Settings](/api-reference/sms/settings), verify end to end with
[Send Test SMS](/api-reference/sms/test), pull the same per-number send
breakdown shown on the detail page with [Get SMS
Usage](/api-reference/sms/usage), and release a number you no longer need
with [Remove SMS Number](/api-reference/sms/delete-number)
* **CLI** - `sequenzy sms settings` shows add-on status, credits, and numbers
(with their labels and prefix overrides), `sequenzy sms label` sets or
clears a label, and `sequenzy sms update` edits a number's label and brand
prefix override in one call;
`sequenzy sms send-test` sends a real test message (pass `--from-number` to
test a specific number), `sequenzy sms usage` shows the per-number send
breakdown, and `sequenzy sms release` removes a number you no longer need;
`sequenzy generate sms ""` drafts message variants; SMS steps work in
`sequenzy sequences create --steps-*`, `update --insert-steps-*`, and
`update --sms-steps-*`
* **MCP** - the `generate_sms`, `get_sms_settings`,
`update_sms_number_label`, `send_test_sms`, and `release_sms_number` tools
plus SMS steps in `create_sequence`, `insert_sequence_step`, and
`update_sequence.smsSteps` let AI agents build and maintain SMS journeys end
to end
* **Outbound webhooks** - subscribe to `sms.sent`, `sms.delivered`,
`sms.failed`, and `sms.opted_out` events (included in the default event set
for new webhooks) to track SMS delivery from your own systems; see
[Outbound Webhooks](/integrations/outbound-webhooks)
Generated and API-provided messages should never include opt-out text or a
brand prefix - Sequenzy adds the brand prefix to every message and the
compliance footer to the first message automatically.
## Related
* [Subscribers](/concepts/subscribers)
* [Sequences](/concepts/sequences)
* [Create subscriber API](/api-reference/subscribers/create) - set `phone` and
`smsConsent` programmatically
* [Generate SMS API](/api-reference/generate/sms)
* [SMS settings API](/api-reference/sms/settings)
# Subscribers
Source: https://docs.sequenzy.com/concepts/subscribers
Understanding subscriber management, lifecycle, and attributes
Subscribers are the core of Sequenzy. They represent contacts in your email marketing database: people who receive your campaigns, transactional emails, and automated sequences.
## Create a campaign for a selected group
Apply filters, search or select contact rows to reveal **Create campaign**. Choose a template, existing email, blank draft or AI prompt. Every matching contact across pages is saved directly in the draft; list membership and existing sequence enrollments do not change. New matching contacts do not join automatically, and eligibility is checked again before sending. Saved lists and segments keep their existing campaign and sequence creation actions. You can also [create a draft for a group](../api-reference/campaigns/from-audience) through the API, CLI or MCP.
## Subscriber Data Model
Each subscriber has the following properties:
| Property | Type | Description |
| - | - | - |
| `email` | string | Unique email address (optional for phone-only SMS contacts) |
| `phone` | string | Phone number in E.164 format |
| `firstName` | string | First name |
| `lastName` | string | Last name |
| `timezone` | string | IANA timezone (e.g. `America/New_York`) for recipient-local campaign delivery |
| `status` | enum | Current status: `active`, `unsubscribed`, `bounced` |
| `tags` | string\[] | Array of tag names attached to subscriber |
| `customAttributes` | object | Flexible key-value store for custom data |
| `createdAt` | datetime | When the subscriber was created |
| `updatedAt` | datetime | When the subscriber was last modified |
### Phone-Only Contacts
A contact can exist with only a phone number and no email address - for
example, an SMS-focused list uploaded from another platform. Phone-only
contacts:
* Can be created through the CSV import (map a phone column and leave email
unmapped), the API, the CLI, and MCP
* Are automatically excluded from email campaigns and skipped by sequence
email steps
* Receive SMS campaigns and sequence SMS steps normally once they have
[SMS consent](/concepts/sms#sms-consent)
* Deduplicate by phone number: importing the same phone again updates the
existing contact instead of creating a duplicate
## Subscriber Status
Subscribers can have one of three statuses:
### Active
The subscriber can receive emails. This is the default status when a subscriber is created.
```json theme={null}
{ "status": "active" }
```
### Unsubscribed
The subscriber has opted out. They will not receive campaigns or sequences, but may still receive transactional emails if required.
```json theme={null}
{ "status": "unsubscribed" }
```
### Bounced
The email address is invalid or unreachable. Sequenzy automatically updates this status when emails bounce.
```json theme={null}
{ "status": "bounced" }
```
Never send marketing emails to unsubscribed or bounced subscribers. Sequenzy
automatically filters them from campaigns.
## Double Opt-In
Double opt-in lets you require email confirmation before new signups become active subscribers. While confirmation is pending, Sequenzy stores the contact as `unsubscribed`, holds target list membership and signup tags, and sends a confirmation email. After the subscriber clicks the confirmation link, Sequenzy activates the subscriber, applies the held lists and tags, and triggers matching automations.
Learn how to enable confirmation emails for subscriber signups.
## Custom Attributes
Custom attributes allow you to store any data about subscribers. Use them for:
* User IDs from your application
* Subscription plans or tiers
* Geographic information
* Purchase history
* Preferences
* Dates and timestamps like signup time, renewal date, or latest checkout attempt
```json theme={null}
{
"customAttributes": {
"userId": "usr_12345",
"plan": "pro",
"company": "Acme Inc",
"industry": "technology",
"mrr": 99.99,
"signupSource": "google-ads"
}
}
```
### Attribute Types
Each attribute value keeps the JSON type you send: string, number, boolean, or a list of those. Keep the same type for an attribute across contacts so segment filters and conditions compare values consistently. Send zip codes, phone-like IDs and other values with leading zeros as strings, for example `"zip": "02134"`, because a number drops the leading zero.
In the dashboard, **+ add** on a contact's Attributes card lists the attributes your other contacts already have and fills in their type. Each attribute shows a type selector for switching between string, number and boolean. Automatic detection keeps values with leading zeros and numbers longer than 15 digits as strings. CSV imports in the dashboard use the same rule when they detect a column's type, so a zip column stays text when any value has a leading zero.
To see the names and types already in use, call [List Subscriber Attributes](../api-reference/subscribers/attributes), run `sequenzy subscribers attributes`, or use the `list_subscriber_attributes` MCP tool. In the CLI, `--attr key=value` always sends strings. Use `--attrs-json` on `subscribers add` and `subscribers update` for other types.
### Using Attributes in Emails
Custom attributes are available as template variables:
```html theme={null}
Hi {{FIRST_NAME}},
Your current plan: {{plan}}
Company: {{company}}
```
### Using Attributes in Conditions
Filter campaigns and create sequence conditions based on attributes:
* `field_equals`: Check exact match
* `field_contains`: Check if value contains string
* `field_greater_than`: Numeric comparison
* `field_less_than`: Numeric comparison
Date and datetime attributes also work in these comparisons. If you send ISO
date values like `2026-03-20` or full timestamps like
`2026-03-19T10:56:03+00:00`, Sequenzy stores them in a way that supports exact
matches and before/after-style range filters.
### Commerce Attributes
Commerce integrations can sync revenue and order metrics onto subscriber profiles.
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly recurring revenue from billing providers |
| `ltv` | Number | Lifetime value |
| `aov` | Number | Average order value |
| `ordersCount` | Number | Total known order count |
| `totalOrders` | Number | Alias for total known order count |
| `totalSpent` | Number | Total known store spend |
The subscriber table shows revenue metrics directly, including `AOV` when either `aov` or legacy `averageOrderValue` is present.
Older commerce data may include `totalRevenue` or `averageOrderValue`.
Sequenzy treats `totalRevenue` as an `ltv` fallback and `averageOrderValue` as
an `aov` fallback so subscriber views do not show duplicate cards for the same
metric.
### Purchased Products
Subscriber profiles show purchased products when Sequenzy has product-level purchase data.
* Stripe purchased products come from Stripe payment history and product catalog data
* Shopify and WooCommerce purchased products come from `ecommerce.order_placed` line items
* Product cards can show image, name, provider, variant, quantity, price, and last purchase time when those fields are available
For Shopify and WooCommerce, make sure order events include `lineItems[].providerProductId`. Variant-level display and matching also uses `lineItems[].providerVariantId`.
## Tags
Tags are labels that categorize subscribers. They're essential for:
1. **Segmentation** - Target campaigns to specific groups
2. **Automation Triggers** - Start sequences when tags are added
3. **Filtering** - Find subscribers with specific characteristics
Deep dive into tag management and best practices
## Lists And Visibility
Subscriber lists can be either public or private.
* Public lists appear on the hosted subscriber email preferences/unsubscribe page. Their names are subscriber-facing, and subscribers can subscribe or unsubscribe from them individually. List descriptions remain internal workspace metadata and are never shown on this public page.
* Private lists are hidden by default. Enable **Allow members to unsubscribe** when you create or edit a private list to show its name only to existing members in preferences. Members can opt out, after which the list disappears. Global resubscribe does not rejoin private lists with this setting enabled; an authorized workspace user must add the member again. Other subscribers cannot discover or join it. Descriptions stay internal. Leaving a list does not cancel a paid membership or stop emails targeted through other lists.
New lists are public by default. When you create a public list, name it as a subscriber-facing category such as `Newsletter` or `Product Updates`. Internal cohort names and operational buckets are usually a better fit for private lists.
If no lists are visible to the subscriber, the hosted page shows a single Unsubscribe button for the global opt-out instead of per-list toggles, and an already-unsubscribed visitor sees a Resubscribe option there.
List privacy controls visibility in the preference center; it does not override a subscriber's global unsubscribe. Choosing to unsubscribe from all email still unsubscribes the subscriber from every active list membership, including private lists.
You can also set `allowMemberUnsubscribe` through the list API, `sequenzy lists create|update --allow-member-unsubscribe` (disable with `--no-allow-member-unsubscribe`), or the MCP `create_list` and `update_list` tools. Existing lists default to disabled.
### Default Lists
Plenty of things create a contact without saying which list it belongs in: a form without list targeting, an API write that omits `listIds`, an event or tag action for an unknown address, a CSV import, and any integration that has no per-integration list targeting of its own. The **Default Lists** setting decides where those contacts land.
Find it in the dashboard under **Contacts -> Lists -> Default Lists**. It is not under Settings.
There are three modes, and they are genuinely different:
| Mode | API value | New untargeted contacts join |
| - | - | - |
| All lists | `null` | Every current and future list, including private lists |
| No lists | `[]` | Nothing - they exist as contacts with no membership |
| Specific lists | array of IDs | Exactly the lists you pick |
The setting applies automatically when a write **creates** a contact, not on
every write. An ordinary profile or attribute upsert for an active contact
leaves their list memberships alone unless it names lists explicitly, so a
background sync cannot pull long-standing contacts into a list created after
they signed up. Explicit subscription and lifecycle reactivation flows are the
exceptions: capture submissions (including forms, popups, landing pages, and
connected-site forms) and a confirmed provider re-creation can reapply target
or default lists so the returning contact actually rejoins them.
Read and change it from the API, CLI, and MCP as well:
```bash theme={null}
# Read the current value
sequenzy companies get comp_abc123
# Point new untargeted contacts at one curated list
sequenzy companies update comp_abc123 --default-list list_abc
# Or keep them out of every list
sequenzy companies update comp_abc123 --default-lists-none
```
Over the API it is the `defaultSubscriberListIds` field on [get company](/api-reference/companies/get) and [update company](/api-reference/companies/update); over MCP it is the same field on `get_company` and `update_company`. Changing it needs the `companies:manage` scope.
Check this setting before connecting a store, payment, or analytics
integration. A provider without its own list targeting - Dodo Payments,
PostHog, Polar, Paddle, WooCommerce - sends contacts created by live
ingestion to whatever the workspace default points at. Payment-provider
backfills do the same. PostHog history imports are an exception: contacts
created by that import have no list memberships.
If that is a lead-magnet or webinar list, a later campaign aimed at "the
people who opted in" will reach the imported customers too.
Changing the default only affects later writes. Nobody is moved or removed
retroactively, so a list that has already been filled needs cleaning up with
[remove subscribers from a list](/api-reference/lists/remove-subscribers) or
`sequenzy lists remove-subscribers`.
## Subscriber Lifecycle
```
┌─────────────────┐
│ Created │
│ (via API or │
│ dashboard) │
└────────┬────────┘
│
▼
┌─────────────────┐
┌─────────│ Active │──────────┐
│ └────────┬────────┘ │
│ │ │
Unsubscribe Hard Bounce Sequences
│ │ Campaigns
▼ ▼ Transactional
┌─────────────────┐ ┌─────────────────┐ │
│ Unsubscribed │ │ Bounced │ │
└─────────────────┘ └─────────────────┘ │
│ │ │
└──────────────────┼───────────────────┘
│
▼
┌─────────────────┐
│ Tracking │
│ (opens, clicks) │
└─────────────────┘
```
## Creating Subscribers
Start on the **Subscribers** page. Select **Add contacts**, then choose whether
to add contacts manually or import a CSV.
### Add Manually
Enter one or more email addresses or phone numbers, choose any lists and tags,
and optionally enroll the new contacts in sequences.
### Import A CSV
For a larger batch, choose **Add contacts → Import CSV**, then upload a file or
paste CSV data. Confirm the email or phone column, map any optional contact
fields, and add tag or custom-attribute rules before previewing the import.
CSV imports are validated contact by contact: you record the source and confirm
permission, and each contact goes through validation and suppression handling.
API, CLI, and MCP imports use the same contact-level validation.
#### Statuses from another platform
Exports from AWeber, Mailchimp, Kit, MailerLite, Brevo, ActiveCampaign and
beehiiv include each contact's status. The importer maps a column named like
`Status`, `Subscriber Status`, `Email Status`, `Subscription Status`, `State`
or `Consent` to the **Status** field, which you can change in the Field mapping
step. The field only appears when your file has a status column. An
`Unsubscribe Date` or `unsubscribed_at` column is picked up automatically.
| Value in your file | Imported as |
| - | - |
| Subscribed, Active, Confirmed, Opted in, Yes | Active |
| Unsubscribed, Opted out, Cancelled, Inactive, Deleted, Suppressed, Complained, Spam | Unsubscribed |
| Bounced, Cleaned, Hard bounce, Undeliverable | Bounced |
| Not confirmed, Unconfirmed, Pending | Active |
| Empty or anything else | Active, counted separately |
A filled unsubscribe date makes that row unsubscribed even when the file has no
status column. Contacts who never confirmed their subscription in the other
platform import as active. The preview shows how many contacts land in each
status before you import.
Unsubscribed and bounced contacts are kept on record but are never emailed,
never enrolled in sequences and never sent a double opt-in email by the import.
An active row never resubscribes a contact who already unsubscribed or bounced.
With **Merge** or **Overwrite**, an unsubscribed or bounced row applies that
status to an existing active contact; **Skip** leaves existing contacts as they
are.
### Reconciling An Import
Custom-attribute segment counts may take roughly 30–35 seconds, or longer when
background processing is busy, to reflect imported updates. Attribute sync
can still be pending after the import shows `completed`. Wait and recheck
the segment count before using it to verify your imported audience.
Every row an import excludes is accounted for. When an import finishes, the
skipped-reason counts add up to the number of skipped rows, the failed-reason
counts add up to the number of failed rows, and added + updated + skipped +
failed equals the number of rows you submitted. The dashboard shows the reasons
in the import banner's issue list; the API, CLI, and MCP return the same counts
on the import record. See
[Get Subscriber Import](/api-reference/subscribers/import-status) for the list
of reasons.
### Via API
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"firstName": "Jane",
"lastName": "Smith",
"tags": ["lead", "saas.monthly"],
"enrollInSequences": true
}'
```
### Auto-Creation
Subscribers are automatically created when you:
* Add a tag to a non-existent email
* Trigger an event for a non-existent email
* Send a marketing email (`emailType: "marketing"`) through the transactional API
* Capture a native Webflow form submission through the
[Webflow integration](/integrations/webflow)
This makes integration easier: you don't need to create subscribers before interacting with them.
Auto-created subscribers join whatever [Default Lists](#default-lists) is set to, unless the call targets lists explicitly.
## Updating Subscribers
Use PATCH to update subscriber data:
```bash theme={null}
curl -X PATCH "https://api.sequenzy.com/api/v1/subscribers/user@example.com" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Janet",
"customAttributes": {
"plan": "pro",
"upgradedAt": "2024-01-15"
}
}'
```
By default, `customAttributes` replaces the existing public custom-attribute
map. Add `"customAttributesStrategy": "merge"` to the PATCH body to retain
unspecified existing keys while overwriting the keys you send.
## Filtering Subscribers
When sending campaigns, you can filter subscribers by:
| Filter | Operators | Example |
| - | - | - |
| Status | `is`, `is_not` | Status is "active" |
| Tags | `contains`, `not_contains` | Tags contains "customer" |
| Email | `contains`, `not_contains`, `is`, `is_not` | Email is "[qa@company.com](mailto:qa@company.com)" |
| Email Provider | `is`, `is_not` | Email Provider is `Google` |
| Date Added | `less_than`, `more_than` | Added after `2026-03-01` |
| Name | `contains`, `not_contains` | First name contains "John" |
Email `is` and `is_not` match the full address and ignore case, so use them for
explicit allowlists and exclusions. Use `contains` for domain or substring
matches like `@company.com`.
For date filters, you can use:
* Rolling relative values like `7 days ago`, `last week`, or `3 days from now`
* Fixed dates like `2026-03-01` or `Mar 1 2026`
`less_than` means "within/after this point" and `more_than` means "before/older than this point". Rolling values update automatically as time passes. Fixed dates stay fixed.
If a custom attribute stores a date or timestamp, you can use the same values in attribute comparisons such as `renewal_date < 3 days from now`.
Email provider detection runs in the background. We normalize major providers into stable buckets such as `Google`, `Microsoft`, and `iCloud`, so Gmail and Google Workspace filter together, Outlook/Hotmail/Microsoft 365 filter together, and dozens of regional providers like UKR.NET, Seznam, Onet, GMX, and Telenet can also be filtered directly.
### Push Filters
Once you set up push in **Settings → Push**, the segment builder adds push filters:
| Filter | Operators | Values | Example |
| - | - | - | - |
| `pushDevice` | `is`, `is_not` | `any`, `web`, `ios`, `android` | Has an active iOS device: `is ios` |
| `pushSent` | `is`, `is_not` | Time window: `7d`, `30d`, ..., `all` | No push in the last week: `is_not 7d` |
| `pushDelivered` | `is`, `is_not` | Time window | Received a push this month: `is 30d` |
| `pushClicked` | `is`, `is_not` | Time window | Never clicked a push: `is_not all` |
`pushDevice` counts only active devices. `pushDelivered` means the device reported showing the notification, which browsers do automatically and apps do when they report it. Test pushes never count. See [Push notifications](./push-notifications).
### Engagement Filters
Engagement filters (`emailSent`, `emailDelivered`, `emailOpened`, `emailClicked`, `emailBounced`, `emailComplained`) accept four kinds of value:
* A rolling time window: `7d`, `30d`, `90d`, `180d`, or `all`
* A specific sent campaign: `campaign:`
* An email-type scope with a time window: `marketing:` or `transactional:`
* A count with a time window: `count:timeRange` (only with the `at_least` / `less_than_count` operators)
Email-type scopes use the delivery policy captured at send time: `marketing` covers campaigns, marketing automations, and Send API calls made with `emailType: "marketing"`; `transactional` covers transactional-policy sends. This is how you avoid over-emailing without transactional traffic muddying the picture. For example, "hasn't received a marketing email in the last week - receipts and password resets don't count":
```text theme={null}
emailSent is_not marketing:7d
```
Ambiguous older automation and Send API events created before a send-time policy snapshot was recorded are not assigned to either scope. Unscoped engagement filters continue to include those events.
Email-type values work with presence operators: `is`, `is_not`, and - for `emailBounced` - `is_temporary_bounce` or `is_permanent_bounce`. They cannot carry a count or be combined with a `campaign:` value.
Count values let you segment by how many times someone opened or clicked, not just whether they did. For example:
* "Best clickers - clicked 10 or more times, ever":
```text theme={null}
emailClicked at_least 10:all
```
* "Suspicious openers - opened 10+ emails but never clicked (likely bot or spamtrap opens)":
```text theme={null}
emailOpened at_least 10:all
emailClicked is_not all
```
* "Fading subscribers - fewer than 2 opens in the last 90 days":
```text theme={null}
emailOpened less_than_count 2:90d
```
Count values cannot be combined with campaign-specific values - a `campaign:` value always checks presence, not a count.
Combining two engagement filters with `AND` lets you target narrow audiences that rolling windows can't express. For example:
* "Subscribers who bounced the launch announcement but did not bounce the follow-up":
```text theme={null}
emailBounced is campaign:cmp_launch
emailBounced is_not campaign:cmp_follow_up
```
* "Subscribers who opened the March newsletter and then clicked the April promo":
```text theme={null}
emailOpened is campaign:cmp_march
emailClicked is campaign:cmp_april
```
Campaign-specific filters are evaluated across all time, so they do not interact with the 7/30/90/180 day windows. To combine "bounced this specific campaign" with "hasn't bounced anything in the last 30 days", use two separate filters.
In the dashboard value picker, rolling windows and sent campaigns now appear in separate sections so you can scroll straight to the campaign you want. The AI filter builder also understands natural phrasing like "bounced the welcome email but not the onboarding email" and resolves the names against your sent campaigns.
### Saved Segment Logic And Counts
Saved segments can combine filters with either `AND` or `OR` logic at every group level:
* Use `AND` when every filter must match
* Use `OR` when any one filter should match
You can also nest groups for expressions such as `last_login within 90 days AND (plan_end is empty OR plan_end is before today)`. In the API, nested segments use the v2 `root` shape with `kind: "group"` and `children` containing filter leaves or more groups. Existing flat `filters` plus `filterJoinOperator` requests still work.
Array-of-object attributes use wildcard paths such as
`history_events[].eventvenue_id`. When two or more positive filters on the
same array path are connected only by `AND`, one shared array element must
satisfy every condition. For example, a 2010 visit at one venue plus an
unrelated recent booking elsewhere does not satisfy "visited this venue
recently." Negative filters (`is_not`, `not_contains`, and `is_empty`) combine
independently because each means that no array element may match its condition.
Saved segments support two additional segment-only fields:
* `event` - filters by custom subscriber events. Use values like `saas.purchase:30d`, `saas.purchase:all`, or `saas.purchase:5:30d` with `at_least` / `less_than_count`.
* `segment` - references another saved segment by ID, so you can compose one segment from another. Sequenzy protects evaluation from cycles and excessive nesting depth.
In the dashboard, Sequenzy may show two different counts for a saved segment:
* **Matched contacts** - every contact that matches the segment definition, regardless of status
* **Active subscribers** - matched contacts who are still eligible for campaigns
Campaigns only send to active subscribers, so campaign recipient previews use the active count rather than the broader matched-contact total.
### Suggested Segments
The **Segments** tab on the Subscribers page suggests segments you haven't created yet. Ready-made templates matched to your business type are always available, and **Suggest with AI** analyzes what is actually in your workspace - your tags, custom attributes, recently fired events, lists, and existing segments - and proposes segments built from that data. Each AI suggestion is verified against your subscribers before it is shown, so every card carries a live subscriber count and suggestions that would match nobody are dropped. Clicking a suggestion opens it prefilled in the segment editor; nothing is saved until you review and confirm it. Generated suggestions stay on the page across visits until you use them or generate new ones.
Each AI suggestion also carries a recommended next step. After you save the segment, one click either opens a new campaign with its audience already set to that segment, or opens the sequence template picker with the new sequence set to trigger when contacts enter the segment.
### Date Filter Examples
**Rolling relative examples**
```text theme={null}
Added less_than 30 days ago
Added more_than 90 days ago
Added less_than 7 days ago
```
**Fixed date examples**
```text theme={null}
Added less_than 2026-03-01
Added more_than Mar 1 2026
```
**Custom attribute date examples**
```text theme={null}
trial_ends_at lte 3 days from now
renewal_date gt 2026-04-01
last_seen_at gte Mar 15 2026
```
## Best Practices
### 1. Capture Data at Sign-up
Collect custom attributes when users first sign up:
```javascript theme={null}
await sequenzy.subscribers.create({
email: user.email,
firstName: user.firstName,
customAttributes: {
userId: user.id,
signupDate: new Date().toISOString(),
source: utm_source,
medium: utm_medium,
campaign: utm_campaign,
},
enrollInSequences: true,
});
```
### 2. Keep Attributes Updated
Sync changes from your app to Sequenzy:
```javascript theme={null}
// When user updates profile
await sequenzy.subscribers.update(user.email, {
firstName: newFirstName,
customAttributes: {
timezone: newTimezone,
language: newLanguage,
},
});
```
### 3. Use Tags for Segments
Tags are more flexible than custom attributes for segmentation:
```javascript theme={null}
// Good: Use tags for states
await sequenzy.subscribers.addTag(email, "customer");
// Avoid: Using boolean attributes for states
await sequenzy.subscribers.update(email, {
customAttributes: { isCustomer: true }, // Less flexible
});
```
### 4. Clean Your List
Regularly remove bounced and unengaged subscribers:
* Remove hard bounces immediately
* Consider removing subscribers who haven't opened in 6+ months
* Use re-engagement campaigns before removing inactive subscribers
## Background tagging
Adding tags from Contacts runs as a background task. You can leave the page and return to **Background tasks** to check progress, inspect failures or cancel future work. Tagging in Contacts enables tag automations; API, CLI and MCP tagging defaults to skipping automations unless you explicitly enable them.
Selection reads live pages before applying tags and excludes contacts created after the request. It is not a point-in-time snapshot: changes to a contact during selection can affect membership. Once selection finishes, tagging uses those selected IDs; changing tags cannot pull additional contacts into the same task. Deleted contacts are counted as failed.
Inspect `processed`, `succeeded`, `failed` and `failures`, including on completed tasks. The failure sample contains up to twenty contacts. If a worker stops while updating a contact, the task reports an uncertain outcome and does not automatically repeat that action. Check that contact before starting another tagging task. Cancellation keeps applied tags and reports an update already in flight as uncertain.
Use `subscribers:read` to inspect tasks. Tagging also requires `subscribers:tag`; creating tag definitions requires `tags:write`, and enabling automations requires `automations:trigger`. Personal keys retain your current company role restrictions. Workers recheck permissions as they process pages.
```bash theme={null}
sequenzy subscribers operations start --kind add_tags --tag vip --request-key september-tags --json
sequenzy subscribers operations list --json
sequenzy subscribers operations get OPERATION_ID --json
sequenzy subscribers operations cancel OPERATION_ID
```
Reuse the same `requestKey` and settings after a timeout or interrupted start response. The CLI prints its chosen recovery key before sending the request. Different settings with the same key return `409`. Tasks have a seven-day processing deadline; terminal records are retained for seven days afterward. See [Start a Subscriber Operation](../api-reference/subscribers/operation-start) for the HTTP contract.
## Capture submission answers
Open your form, popup or landing page and choose **Submissions** to view saved contact details and answers,
filter by an exact custom field name and answer, or export matching records.
Snapshots preserve the submitted answers even if a contact changes later.
Tracking starts with new submissions; existing profile attributes are not
backfilled as historical submissions. Deleting the contact or source removes its
records. See [List Form Submissions](../api-reference/widgets/list-capture-submissions).
Recovered historical records are marked **Reconstructed**. Their displayed date is the historical event time, and their form association is inferred. Missing answers are not filled in from current contact profiles.
## Related
Learn about tagging and segmentation
Track subscriber actions with events
Automate emails based on subscriber data
Subscribers API documentation
# Sync Rules
Source: https://docs.sequenzy.com/concepts/sync-rules
Automatically manage tags based on events
Sync rules automatically add and remove tags when events occur. They're the glue between events and tags, enabling powerful automation without manual intervention.
## How Sync Rules Work
When an event is triggered for a subscriber:
```
Event: "saas.purchase"
│
▼
┌─────────────────────────┐
│ Check Sync Rules │
│ for this event │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Add Tags: "customer" │
│ Remove: "lead", │
│ "trial" │
└─────────────────────────┘
```
Sync rules are managed in Settings, or programmatically: the
[API](/api-reference/sync-rules/get) exposes `GET`/`PUT /api/v1/sync-rules`,
the CLI has `sequenzy sync-rules get` and `sequenzy sync-rules update`, and
the MCP server provides `get_sync_rules` / `update_sync_rules` tools.
## Optional Platform Preset
New workspaces start with no sync rules, so importing or subscribing a contact
does not silently assign a business-stage tag. You can add rules in Settings or
opt into Sequenzy's legacy platform preset for common SaaS and ecommerce events.
The preset contains these rules:
| Event | Tags Added | Tags Removed | Condition |
| - | - | - | - |
| `saas.purchase` | `customer` | `lead`, `past-due`, `cancelled`, `churned`, `visited-payment-page` | |
| `saas.purchase.monthly` | `customer`, `saas.monthly` | `lead`, `past-due`, `cancelled`, `churned`, `saas.yearly`, `visited-payment-page` | |
| `saas.purchase.yearly` | `customer`, `saas.yearly` | `lead`, `past-due`, `cancelled`, `churned`, `saas.monthly`, `visited-payment-page` | |
| `saas.payment_failed` | `past-due` | None | Only if tagged `customer` |
| `saas.cancelled` | `cancelled` | `customer` | |
| `saas.churn` | `churned` | `customer`, `cancelled`, `trial`, `past-due`, `saas.monthly`, `saas.yearly` | |
| `saas.refund` | `refunded` | `customer` | |
| `saas.trial_started` | `trial` | `lead` | |
| `saas.trial_ended` | None | `trial` | |
| `saas.trial_cancelled` | `cancelled` | None | |
| `saas.checkout_expired` | `visited-payment-page` | None | Only if not tagged `customer` |
| `contact.subscribed` | `lead` | None | |
| `ecommerce.product_added_to_cart` | `ecommerce.in_cart` | `ecommerce.in_checkout` | |
| `ecommerce.cart_started` | `ecommerce.in_cart` | `ecommerce.in_checkout` | |
| `ecommerce.cart_cleared` | None | `ecommerce.in_cart`, `ecommerce.in_checkout` | |
| `ecommerce.checkout_started` | `ecommerce.in_checkout` | `ecommerce.in_cart` | |
| `ecommerce.order_placed` | `ecommerce.customer` | `ecommerce.in_cart`, `ecommerce.in_checkout` | |
| `ecommerce.customer_created` | `lead` | None | |
| `ecommerce.order_refunded` | `refunded` | None | |
The platform preset is not enabled automatically for new workspaces. Review
its SaaS/ecommerce semantics before enabling it for a services, consulting, or
other relationship-led business.
Legacy workspaces that still inherit platform defaults continue receiving future
updates to that preset for compatibility. Once you save a customized rule set,
it replaces the preset entirely. "Reset to Defaults" in Settings opts back into
the inheriting preset, and saving an empty rule set disables sync rules completely.
Sync rules also run for events Sequenzy records natively - engagement events (`email.opened`, `email.clicked`, `email.replied`), delivery events (`email.bounced`, `email.unsubscribed`, `contact.unsubscribed`), and integration events like `ecommerce.back_in_stock_requested`. Rules on bounce and unsubscribe events only change tags; they never start a sequence for a suppressed contact.
## Sync Rule Anatomy
Each sync rule defines:
```json theme={null}
{
"event": "saas.purchase",
"addTags": ["customer"],
"removeTags": ["lead", "past-due", "cancelled", "churned"]
}
```
| Field | Description |
| - | - |
| `event` | The event name that triggers this rule |
| `addTags` | Tags to add when the event fires |
| `removeTags` | Tags to remove when the event fires |
Rules can also carry optional conditions: required subscriber tags, excluded
subscriber tags, and (for commerce events) a product match.
## Product Match Conditions
Commerce events that carry products - order placed, product viewed, added to
cart, and similar - can be filtered by the product itself. Add a **Product
match** condition and the rule only runs when at least one product on the
event matches your selected product tags, collections, or product types.
This is how you build ownership audiences from purchases. For example, a
record store tags every vinyl product with `Vinyl` in Shopify, then adds a
rule:
```json theme={null}
{
"event": "ecommerce.order_placed",
"addTags": ["vinyl-collector"],
"conditions": {
"purchasedProduct": { "tags": ["Vinyl"] }
}
}
```
Anyone who buys a vinyl record or accessory is tagged `vinyl-collector`
automatically, ready to receive campaigns for that interest. Within a product
match, values in the same selector are OR'd (any of these tags) and different
selectors are AND'd (must match the tag AND the collection).
Rules with a product match never run for events without product context, so
they are safe to combine with tag conditions.
## Execution Order
When an event triggers:
1. **Event recorded** - Stored in analytics
2. **Remove tags** - Listed tags are removed first
3. **Add tags** - Listed tags are added
4. **Automations checked** - Sequences triggered by new tags
This order ensures clean state transitions.
## Use Cases
### Customer Lifecycle
Track where subscribers are in their journey:
```
Event: saas.trial_started
Add: "trial"
Remove: "lead"
Event: saas.purchase
Add: "customer"
Remove: "lead", "trial", "past-due"
Event: saas.cancelled
Add: "cancelled"
Remove: "customer"
Event: saas.trial_cancelled
Add: "cancelled"
Event: saas.churn
Add: "churned"
Remove: "customer", "cancelled"
```
**Result:** Each lifecycle change replaces the previous stage tag, so a subscriber does not keep stale tags such as `trial` after converting.
### Payment Status
Track payment health:
```
Event: saas.payment_failed
Add: "past-due"
Event: saas.purchase
Remove: "past-due"
```
### Subscription Tiers
When integrated with payment providers:
```
Event: subscription.upgraded_to_pro
Add: "pro-plan"
Remove: "starter-plan", "free-tier"
Event: subscription.downgraded_to_starter
Add: "starter-plan"
Remove: "pro-plan", "enterprise-plan"
```
## Custom Sync Rules
Create custom rules in Settings → Events & Sync Rules:
### Example: Lead Scoring
```
Event: page_viewed_pricing
Add: "high-intent"
Event: demo_requested
Add: "demo-lead", "high-intent"
Remove: "cold-lead"
Event: demo_completed
Add: "demo-completed"
Remove: "demo-lead"
```
### Example: Engagement Tracking
```
Event: email.opened
Add: "engaged"
Remove: "inactive"
Event: product_used
Add: "active", "engaged"
Remove: "inactive", "at-risk"
```
### Example: Onboarding Progress
```
Event: onboarding_step_1_completed
Add: "onboarding-started"
Remove: "new-signup"
Event: onboarding_completed
Add: "onboarded"
Remove: "onboarding-started"
```
## Sync Rules + Sequences
Sync rules work seamlessly with sequences:
```
1. Event: "saas.purchase"
│
▼
2. Sync Rule Applied
Add: "customer"
│
▼
3. Sequence Trigger
"When tag 'customer' added"
│
▼
4. Sequence Starts
Customer onboarding flow
```
This pattern lets you:
* Keep event handling simple (just fire events)
* Centralize tag logic (sync rules)
* Build flexible sequences (tag-based triggers)
## Payment Provider Integration
When you connect Stripe or other payment providers:
### Automatic Event Firing
The integration automatically fires events:
| Stripe Event | Sequenzy Event |
| - | - |
| `customer.subscription.created` | `saas.purchase` |
| `customer.subscription.deleted` | `saas.cancelled` |
| `invoice.payment_failed` | `saas.payment_failed` |
| `customer.subscription.updated` (downgrade) | `saas.downgrade` |
| `customer.subscription.updated` (upgrade) | `saas.upgrade` |
### Automatic Customer Sync
Stripe customers are synced as subscribers with:
* Email from Stripe customer
* Name from Stripe customer
* Custom attributes for subscription data
## Best Practices
### 1. Design Mutually Exclusive Tags
For lifecycle states, ensure only one tag at a time:
```
✓ Sync rules that remove old state when adding new
Event: saas.purchase
Add: "customer"
Remove: "lead", "trial", "churned"
✗ Tags that can accumulate
Event: saas.purchase
Add: "customer"
(lead and trial remain)
```
### 2. Use System Tags Consistently
Stick to the built-in tag names:
```
✓ "customer", "lead", "trial", "churned"
✗ "is-customer", "isLead", "in_trial"
```
### 3. Test Your Rules
Before going live:
1. Create a test subscriber
2. Trigger events via API
3. Verify tags are added/removed correctly
4. Check that sequences trigger
### 4. Document Custom Rules
Keep a reference of your custom sync rules:
| Event | Add | Remove | Purpose |
| - | - | - | - |
| `feature_activated` | `power-user` | `new-user` | Track activation |
| `support_ticket_opened` | `needs-support` | None | Flag for attention |
| `support_ticket_resolved` | None | `needs-support` | Clear flag |
### 5. Don't Overcomplicate
Start with default rules, add custom ones as needed:
```
✓ Start simple
- Use default SaaS events
- Add custom rules for specific needs
✗ Over-engineer
- Custom rule for every possible event
- Complex tag dependencies
```
## Debugging Sync Rules
### Check Event Fired
View subscriber's event history in dashboard.
### Check Tags Applied
View subscriber's current tags after event.
### Common Issues
| Issue | Cause | Solution |
| - | - | - |
| Tag not added | Event name mismatch | Check event name exactly |
| Old tag not removed | Not in `removeTags` | Add to removeTags array |
| Sequence not triggering | Tag trigger doesn't match | Verify sequence trigger |
## Related
Events that trigger sync rules
Tags managed by sync rules
Automations triggered by tags
Contacts that receive tags
# Tags
Source: https://docs.sequenzy.com/concepts/tags
Segment subscribers and trigger automations with tags
Tags are labels that help you organize, segment, and automate interactions with your subscribers. They're one of the most powerful features in Sequenzy.
## What Are Tags?
Tags are simple string labels attached to subscribers. Each subscriber can have multiple tags, and tags can be added or removed at any time.
```json theme={null}
{
"email": "user@example.com",
"tags": ["customer", "pro-plan", "newsletter", "webinar-attendee"]
}
```
## Why Use Tags?
### 1. Segmentation
Target specific groups in campaigns:
* Send product updates to `customer` tagged subscribers
* Exclude `churned` subscribers from promotional emails
* Target `trial` users with conversion campaigns
### 2. Automation Triggers
Start sequences when tags are added:
* When `customer` tag is added → Start onboarding sequence
* When `churned` tag is added → Start win-back sequence
* When `enterprise` tag is added → Notify sales team
### 3. Personalization
Customize email content by using tags in campaign targeting, automation
branches, or conditional display rules in the email editor. For raw HTML
templates, sync the value you need into a subscriber custom attribute and guard
the section with a simple conditional:
```html theme={null}
{{#if subscriber.isCustomer}}
Thanks for being a valued customer!
{{else}}
Ready to upgrade? Check out our plans.
{{/if}}
```
### 4. Analytics
Track subscriber segments over time:
* How many `trial` users convert to `customer`?
* What's the churn rate for `pro-plan` vs `starter-plan`?
## System Tags
Sequenzy includes built-in system tags for common use cases:
| Tag | Description | Applied by the platform preset |
| - | - | - |
| `lead` | Potential customer | Yes |
| `customer` | Paying customer | Yes |
| `trial` | In trial period | Yes |
| `cancelled` | Cancelled subscription | Yes |
| `churned` | Churned customer | Yes |
| `past-due` | Payment failed | Yes |
| `refunded` | Received a refund | Yes |
| `visited-payment-page` | Started checkout without buying | Yes |
| `saas.monthly` | On a monthly plan | Yes |
| `saas.yearly` | On a yearly plan | Yes |
| `ecommerce.in_cart` | Has items in a store cart | Yes |
| `ecommerce.in_checkout` | Started a store checkout | Yes |
| `ecommerce.customer` | Placed a store order | Yes |
| `active` | Active subscriber, if you track it | No, apply it yourself |
| `inactive` | No recent activity, if you track it | No, apply it yourself |
The preset applies these tags only when you enable it in **Settings → Events &
Sync Rules**, typically together with a billing integration such as Stripe.
## Tag Normalization
Tags are automatically normalized when created:
* Converted to lowercase
* Runs of whitespace replaced with one hyphen
* Other characters, including underscores and punctuation, preserved
```
"Pro Customer" → "pro-customer"
"VIP_User" → "vip_user"
"Newsletter!" → "newsletter!"
```
## Adding Tags
### Single Tag
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"tag": "customer"
}'
```
### Multiple Tags (Bulk)
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags/bulk" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"tags": ["customer", "pro-plan", "2024-cohort"]
}'
```
### Auto-Creation
When you add a tag, Sequenzy automatically:
1. Creates the subscriber if they don't exist
2. Creates the tag definition if it doesn't exist
3. Adds the tag to the subscriber
This makes integration seamless: no pre-configuration required.
## Removing Tags
Tags can be removed through:
1. **Dashboard** - Manual removal in subscriber profile
2. **Sequences** - "Remove Tag" action in automations
3. **Sync Rules** - Automatic removal when events occur
## Using Tags in Sequences
### Trigger: When Tag Added
Start a sequence when a subscriber receives a tag:
```
Trigger: contact_added_tag "customer"
│
▼
┌─────────────────┐
│ Welcome Email │
└─────────────────┘
│
▼
┌─────────────────┐
│ Wait 3 days │
└─────────────────┘
│
▼
┌─────────────────┐
│ Onboarding Tips │
└─────────────────┘
```
### Action: Add/Remove Tags
Modify tags during sequence execution:
```
┌─────────────────┐
│ Send Upgrade │
│ Offer Email │
└─────────────────┘
│
▼
┌─────────────────┐
│ Wait for Event │
│ "purchase" │
│ Timeout: 7 days │
└─────────────────┘
│
├── Event received ────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Add tag: │ │ Add tag: │
│ "converted" │ │ "offer-expired" │
└─────────────────┘ └─────────────────┘
```
### Condition: Has Tag
Branch logic based on tags:
```
┌─────────────────────┐
│ Condition: │
│ has_tag "customer" │
└─────────────────────┘
│
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Upsell │ │ Convert │
│ Email │ │ Email │
└─────────┘ └─────────┘
```
## Using Tags in Campaigns
### Filter by Tags
When creating a campaign, filter recipients:
**Include subscribers with tags:**
* `customer` AND `active`
**Exclude subscribers with tags:**
* `churned` OR `unsubscribed`
### Example: Feature Announcement
Target active customers on pro plan:
```
Recipients: All subscribers
Filters:
- tags contains "customer"
- tags contains "pro-plan"
- tags not_contains "churned"
```
## Dashboard Visibility
In **Settings → Tags & Labels**, Sequenzy shows:
* How many unique contacts were tagged with each tag in the last 24 hours
* Which active sequences currently trigger when that tag is added
* Which paused sequences would trigger if you reactivate them
You can also see the same sequence-trigger context on a subscriber's activity timeline when a tag is added, which helps you confirm whether a tag actually entered the right automation.
## Sync Rules
Sync rules automatically manage tags based on events. When an event occurs, specified tags are added and removed.
Configure automatic tagging based on events
### Platform Preset
New workspaces start with no sync rules. You can opt into Sequenzy's platform
preset, which tags SaaS and ecommerce lifecycle events automatically. See the
full preset table in [Sync Rules](/concepts/sync-rules#optional-platform-preset).
## Best Practices
### 1. Use Descriptive Names
```
✓ "webinar-jan-2024-attendee"
✓ "ebook-marketing-guide-downloaded"
✓ "pro-plan-annual"
✗ "tag1"
✗ "promo"
✗ "temp"
```
### 2. Create a Tagging Strategy
Define your tag taxonomy before implementation:
**Lifecycle tags:**
* `lead`, `trial`, `customer`, `churned`
**Plan tags:**
* `free-plan`, `starter-plan`, `pro-plan`, `enterprise-plan`
**Behavior tags:**
* `email-opened-30d`, `highly-engaged`, `at-risk`
**Source tags:**
* `source-organic`, `source-paid`, `source-referral`
### 3. Avoid Tag Proliferation
Too many tags become unmanageable. Consolidate similar tags:
```
✗ "clicked-email-jan", "clicked-email-feb", "clicked-email-mar"
✓ "email-engaged" (reset monthly)
✗ "purchased-product-a", "purchased-product-b", "purchased-product-c"
✓ "customer" + customAttribute: purchasedProducts
```
### 4. Use Events for Temporary States
For actions that happen multiple times, use events instead of tags:
```
✗ Tag: "viewed-pricing-page" (adds up over time)
✓ Event: "page_viewed" with properties: { page: "pricing" }
```
### 5. Document Your Tags
Maintain a reference of what each tag means and how it's applied:
| Tag | Description | Applied By |
| - | - | - |
| `customer` | Has paid subscription | Sync rule: saas.purchase |
| `enterprise` | Enterprise plan customer | Manual |
| `newsletter` | Subscribed to newsletter | Signup form |
## Related
Trigger tag changes with events
Automate tagging based on events
Use tags to trigger and control flows
Add tags via API
# Email Tracking
Source: https://docs.sequenzy.com/concepts/tracking
Track email opens, clicks, and unsubscribes to measure engagement
Sequenzy automatically tracks engagement metrics for all emails you send. This helps you understand how subscribers interact with your emails and optimize your campaigns.
## What We Track
Sequenzy tracks three types of email engagement:
| Metric | Description | How It Works |
| - | - | - |
| **Opens** | When a subscriber opens your email | Invisible tracking pixel loaded by email client |
| **Clicks** | When a subscriber clicks any link in your email | Links are wrapped with tracking redirects |
| **Unsubscribes** | When a subscriber clicks the unsubscribe link | Unsubscribe link clicks are tracked separately |
## How Tracking Works
### Open Tracking
When you send an email, Sequenzy embeds a tiny invisible image (1x1 pixel) at the end of your email. When the subscriber's email client loads this image, we record an open event.
```
Email opened → Tracking pixel loaded → Open recorded
```
Some email clients block images by default or use privacy features that
prevent accurate open tracking. Apple Mail Privacy Protection, for example,
can pre-fetch images, which may inflate open rates. Keep this in mind when
analyzing your metrics.
### Click Tracking
All links in your emails are automatically wrapped with tracking URLs. When a subscriber clicks a link:
1. The click is recorded with the link URL
2. The subscriber is immediately redirected to the original destination
```
Subscriber clicks link → Click recorded → Redirect to destination
```
The redirect is instant and invisible to the subscriber.
### Tracking hostname
Tracked links use your company tracking domain, for example
`links.yourdomain.com`. Every sending domain uses it, so you set it up once, in
**Settings → Tracking** or with [Set Tracking Domain](../api-reference/tracking-domain/set).
It is optional and never blocks sending: until it verifies, or if its DNS record
breaks or you remove it, links use Sequenzy's shared tracking domain. See
[Domain Verification](../guides/domain-verification#custom-tracking-domains).
### Unsubscribe Tracking
The unsubscribe link in your emails is tracked separately from regular clicks. This gives you visibility into:
* How many people are unsubscribing
* Which emails drive the most unsubscribes
* Unsubscribe trends over time
When you turn **Unsubscribe tracking** off, Sequenzy unsubscribe links in subsequent
sends, go directly to `https://sequenzy.com`. This applies even
when you use a custom tracking domain. Footer links and one-click unsubscribe
headers follow the same setting. Actual unsubscribes and their email attribution
are still recorded. Links in emails already delivered keep working as before.
## Disabling Tracking
You can disable tracking for your entire account in your settings. This is useful if:
* You want to respect subscriber privacy
* Your audience is privacy-conscious (e.g., security professionals)
* You're in a regulated industry with strict tracking requirements
* You simply don't need engagement metrics
To disable tracking:
1. Go to **Settings** in your dashboard
2. Navigate to the **Tracking** section
3. Toggle off the tracking options you want to disable
You can disable each tracking type independently:
| Setting | Effect |
| - | - |
| Disable open tracking | No tracking pixel added to emails |
| Disable click tracking | Links are not wrapped with tracking URLs |
| Disable unsubscribe tracking | Unsubscribe links go directly to sequenzy.com |
The account-wide switches above are the master policy for campaigns,
sequences, and Transactional API sends. To keep tracking on for marketing
emails while changing only the Transactional API defaults, expand **Open
tracking** or **Click tracking** and use its nested **Transactional emails**
switch. These transactional defaults are enabled for new workspaces.
Disabling open or click tracking means you won't have that engagement data for
metrics, automation conditions, or segmentation based on email activity.
The same settings are available outside the dashboard, so an agent or script
can audit and change them:
```bash theme={null}
# CLI
sequenzy tracking settings
sequenzy tracking update --opens off --clicks off
sequenzy tracking update --transactional-opens off --transactional-clicks off
```
The API endpoints are [Get Tracking
Settings](/api-reference/tracking-settings/get) and [Update Tracking
Settings](/api-reference/tracking-settings/update), and the MCP tools are
`get_tracking_settings` and `update_tracking_settings`. Changes apply to emails
sent afterwards - emails already sent keep the links and pixels they were
rendered with.
### Per-send opt-out (transactional API)
Transactional API sends use the dashboard's Transactional API Tracking
defaults when `trackingSettings` is omitted. A send can turn off tracking for a
single message without changing those defaults. Pass a `trackingSettings`
object with `clickTracking: false` to skip link rewriting or
`openTracking: false` to skip the open pixel on that send. This is useful when a
click-tracking redirect domain would break iOS/Android universal links or deep
links, so you can keep tracking on account-wide for marketing while sending
those specific emails untracked. These fields only opt out: they cannot enable
tracking that is disabled by either the account-wide or Transactional API
default.
```json theme={null}
{
"to": "user@example.com",
"slug": "password-reset",
"trackingSettings": { "clickTracking": false }
}
```
## Privacy Considerations
Sequenzy is designed with privacy in mind:
* **No third-party tracking**: All tracking data stays within Sequenzy
* **Subscriber data protection**: Tracking data is associated with subscribers, not sold or shared
* **Easy opt-out**: Subscribers can unsubscribe with one click
* **Configurable**: You control what gets tracked
### GDPR Compliance
If you're subject to GDPR, you should:
1. Include tracking disclosure in your privacy policy
2. Consider disabling tracking for EU subscribers if required
3. Honor unsubscribe requests promptly (Sequenzy does this automatically)
## Events Generated
Email tracking automatically generates events that you can use in automations and analytics:
| Tracking Action | Event Generated |
| - | - |
| Email opened | `email.opened` |
| Link clicked | `email.clicked` |
| Unsubscribed | `email.unsubscribed` |
These events can trigger sequences, apply sync rules, and appear in subscriber timelines.
Learn more about events and how to use them in automations
## Best Practices
### 1. Monitor Engagement Trends
Track open and click rates over time to identify:
* Which subject lines perform best
* Optimal send times for your audience
* Content that resonates with subscribers
### 2. Use Tracking Data for Segmentation
Create segments based on engagement:
* **Active subscribers**: Opened or clicked in the last 30 days
* **Inactive subscribers**: No engagement in 90+ days
* **Highly engaged**: Clicked multiple emails
### 3. Respect Unsubscribes
High unsubscribe rates indicate:
* Email frequency is too high
* Content doesn't match subscriber expectations
* List quality issues
Use unsubscribe data to improve your email strategy.
### 4. Consider Your Audience
If your audience is privacy-focused:
* Be transparent about tracking in your emails
* Consider disabling open tracking (less invasive than click tracking)
* Highlight your privacy practices
## Related
Configure automatic campaign attribution for email links
View and analyze your tracking data
Send broadcast campaigns
Create automated email workflows
Manage your subscriber list
# Transactional Emails
Source: https://docs.sequenzy.com/concepts/transactional-emails
Send programmatic, per-user emails via API
Transactional emails are triggered programmatically for individual users: order confirmations, password resets, account notifications, and other time-sensitive communications.
When you create an email from a prompt in the dashboard, your first applied AI result saves automatically. Wait for the save confirmation before refreshing. Later edits still use **Save**; if the automatic save fails, your result stays in the editor and you can click **Save** to retry.
## Transactional vs Marketing
| Transactional | Marketing |
| - | - |
| Triggered by user action | Sent to segments |
| Up to 50 `to` recipients per send, plus `cc` and `bcc` | Multiple recipients |
| Time-sensitive | Can be scheduled |
| Expected by user | Promotional |
| Higher deliverability | May be filtered |
**Examples of transactional emails:**
* Order confirmation
* Shipping notification
* Password reset
* Account verification
* Payment receipt
* Security alert
## Sending Modes
### 1. Template Mode (Recommended)
Create reusable templates in the dashboard, send via API using the template slug.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"slug": "order-confirmation",
"variables": {
"orderNumber": "ORD-12345",
"total": "$99.99",
"deliveryDate": "January 20, 2024"
}
}'
```
**Benefits:**
* Non-developers can edit templates
* Consistent branding
* Analytics per template
* Template blocks can include conditional display rules based on recipient variables
* Repeat blocks can render one child block set per item in an array variable
### Dynamic Array Data
Transactional templates can repeat blocks over arrays passed in `variables`.
For example, a repeat block with source `items` and item alias `item`
renders its child blocks once per item. Inside those children, use scoped merge
tags like `{{item.title}}`, `{{item.description}}`, and `{{item.number}}`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"slug": "order-confirmation",
"variables": {
"items": [
{ "title": "Pro plan", "description": "Monthly subscription" },
{ "title": "Priority support", "description": "Account add-on" }
]
}
}'
```
#### Nested Repeats
Repeat blocks can be nested. Point the inner repeat block's source at an array
inside each parent item using the parent's item alias - it does not need a
separate top-level array. For example, with an outer repeat over `orders` (alias
`order`), an inner repeat with source `order.lineItems` (alias `line`) renders
once per line item of the current order, and its children can use merge tags
like `{{line.title}}`.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"slug": "order-confirmation",
"variables": {
"orders": [
{
"number": "ORD-12345",
"lineItems": [
{ "title": "Pro plan" },
{ "title": "Priority support" }
]
}
]
}
}'
```
### Conditional Blocks
Conditional (if/else) blocks show one set of blocks when a rule matches and an
optional fallback otherwise. In the editor, add a condition and pick the
**Custom variable** field to branch on a value passed in your request `variables`
or an automation `event` payload - not just stored subscriber data.
The variable name is the same reference you would use in a `{{merge tag}}`, so
nested paths work too (for example `order.total` or `event.plan`). You can match
with equals, contains, numeric comparisons (`>=`, `<=`, `>`, `<`), or check
whether the variable is present with "is empty" / "is not empty".
For example, sending with `"variables": { "plan": "pro" }` will render the IF
branch of a condition like `plan equals pro`, and the OTHERWISE branch for any
other value.
Conditions can also target stored subscriber data - segment membership, custom
events, tags, lists, status, engagement, and purchases. These rules are checked
against the recipient's subscriber profile at send time; if the recipient is
not a stored subscriber, they see the OTHERWISE branch.
### 2. Direct Mode
Send email content directly without a pre-created template.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"subject": "Your Order #{{orderNumber}}",
"body": "
Thank you for your order!
Order: {{orderNumber}}
",
"variables": {
"orderNumber": "ORD-12345"
}
}'
```
**Use cases:**
* Dynamic content generated by your app
* One-off emails that don't need templates
* Testing and development
* React Email templates (see below)
#### Track direct-mode emails with `trackAs`
Direct sends are counted in your overall stats but not per email. Add `trackAs`
to group every send of an email your code renders, such as a weekly summary:
```json theme={null}
{
"to": "customer@example.com",
"subject": "Your week in review",
"html": "
Your week
3 new subscribers
",
"trackAs": "weekly-report"
}
```
The first send creates a transactional email marked **Code** in your dashboard.
It shows sends, deliveries, opens, clicks and bounces, plus a snapshot of
recent content. You can't edit it there because your code renders the content,
but you can rename it, label it, or disable it to reject further sends with
that `trackAs`. Use a stable ASCII name for each email type (letters, digits,
spaces and `. _ - : /`), not per-recipient values; each company can have up to 100. The CLI flag is `--track-as` and the MCP
`send_email` field is `trackAs`. See
[Send Email](../api-reference/transactional/send#track-emails-rendered-in-your-code).
### 3. React Email Mode
Build type-safe, responsive email templates using [React Email](https://react.email) components and render them to HTML.
```typescript theme={null}
import { render } from "@react-email/render";
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";
// Create your email component
function OrderConfirmation({ orderNumber, total }: { orderNumber: string; total: string }) {
return (
Your order #{orderNumber} is confirmedThank you for your order!Order: #{orderNumber}Total: {total}
);
}
// Render and send
const html = await render(OrderConfirmation({ orderNumber: "ORD-12345", total: "$99.99" }));
await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "customer@example.com",
subject: "Your Order #ORD-12345",
body: html,
}),
});
```
**Benefits:**
* Type-safe templates with TypeScript
* Reusable components
* Responsive design built-in
* Preview emails during development
### 4. SMTP Template Payloads
If another product can send through SMTP but lets you control the message body, you can point it at Sequenzy SMTP and send a JSON payload that references one of your existing transactional emails.
This is useful for products such as Supabase Auth, where Supabase still triggers the email but Sequenzy renders and sends the final template.
**Requirements:**
* A Sequenzy API key for SMTP authentication
* At least one sender profile on a verified sending domain
* A transactional email with a sender profile assigned
```json theme={null}
{
"slug": "supabase-magic-link",
"dataVariables": {
"confirmation_url": "https://app.example.com/auth/verify?token=123",
"email": "user@example.com"
}
}
```
You can also use `transactionalId` instead of `slug`. If Sequenzy receives a JSON body in this format over SMTP, it ignores the raw email subject/body and sends the saved transactional template instead.
For the full Supabase setup, including variable mapping, see the [Supabase integration guide](/integrations/supabase).
See our quickstart guides for [Bun](/send-email/bun),
[Next.js](/send-email/nextjs), and [Express](/send-email/express).
## Creating Templates
### In the Dashboard
1. Go to **Transactional** in your dashboard
2. Click **Create Template**
3. Enter a **slug** (URL-friendly identifier)
4. Design your email with the visual editor
5. Add variable placeholders where needed
6. Save and activate
### Template Slugs
Slugs are unique identifiers for your templates:
```
order-confirmation
password-reset
welcome-email
invoice-reminder
shipping-update
```
Slugs are auto-generated from the template name but can be customized. They
cannot be changed after creation.
## Variables
Variables let you personalize transactional emails.
### Syntax
```html theme={null}
Hello {{FIRST_NAME}},
Your order {{orderNumber}} has shipped!
```
### With Defaults
Provide fallback values for missing variables:
```html theme={null}
Hello {{FIRST_NAME|Customer}},
Hello {{ firstName | default: "Customer" }},
```
If `FIRST_NAME` or `firstName` is empty, "Customer" is used.
If a variable has no default and no value is provided, it renders as an empty
string. The transactional email is still sent.
### Raw HTML Variables
Variables are always HTML-escaped, so a value like `
...
` renders
as visible code. When a variable's value is trusted HTML that should render as
markup - for example a prerendered order summary passed through the API - add
the `html.` prefix to the tag:
```html theme={null}
{{html.prerenderedHtml}}
```
`{{html.prerenderedHtml}}` inserts the `prerenderedHtml` variable unescaped, so
you can keep the template's branded header and footer while your system
supplies the body HTML per send:
```json theme={null}
{
"to": "customer@example.com",
"slug": "order-confirmation",
"variables": {
"prerenderedHtml": "
Order #1234
"
}
}
```
The prefix goes on the tag, not on the variable name - you still pass
`prerenderedHtml` in `variables`, and `{{prerenderedHtml}}` elsewhere in the
same template stays escaped. `html.` joins the existing `date.`, `urlencode.`,
`lower.` and `upper.` prefixes, so a variable of your own named `html` is not
addressable as `{{html.something}}`. It also composes with them, outermost
first: `{{html.upper.orderSummary}}` uppercases the value and then inserts it
as HTML.
Keep these rules in mind:
* Only pass HTML you generate or trust. Never forward end-user input through a
raw HTML tag; use a regular `{{variable}}`, which stays escaped.
* Injected HTML is sanitized the same way as raw HTML content: scripts, inline
event handlers, and dangerous URLs are stripped.
* The `html.` prefix only applies in HTML text position. Inside attributes such
as `href` or `src`, values are validated and escaped exactly like ordinary
variables.
* Send final HTML. A value is never expanded recursively at the moment it is
inserted, but a template send merges the assembled email once more before it
goes out, so any `{{variable}}` still present in the injected HTML is
substituted on that pass. Resolve your own merge-tag syntax before passing the
HTML in. Values substituted from an ordinary `{{variable}}` into HTML text or
into an HTML attribute are not affected: merge-tag syntax inside them is
neutralized when the value is escaped, so it cannot become a tag on the later
pass.
* Links inside the injected HTML still get click tracking and open tracking as
usual.
The prefix is not limited to transactional variables: it works everywhere merge
tags render, including subscriber custom attributes in
[campaigns](/concepts/campaigns#raw-html-attribute-values) and sequences -
`{{html.subscriber.body_html}}` renders a stored attribute's HTML unescaped.
### Conditional HTML Sections
Use `{{#if variable}}`, `{{else}}`, and `{{/if}}` to render simple conditional
sections in raw HTML. This is most useful with subscriber custom attributes:
```html theme={null}
{{#if subscriber.plan}}
Your {{subscriber.plan}} workspace is ready.
{{else}}
Your workspace is ready.
{{/if}}
```
`{{#unless variable}}...{{/unless}}` is also supported. Conditions check
whether the resolved variable is present and truthy. Nested `if`/`unless`
sections are supported, but helpers, comparisons, loops, and arbitrary
Handlebars expressions are not.
### System Variables
These variables are resolved at send time:
| Variable | Source |
| - | - |
| `{{EMAIL}}` | Recipient email address |
| `{{FIRST_NAME}}` | Request `firstName` aliases, then the matched subscriber's stored first name |
| `{{LAST_NAME}}` | Request `lastName` aliases, then the matched subscriber's stored last name |
| `{{NAME}}` | Request `name`/`fullName`, or a full name built from the resolved first and last names |
Single-recipient REST API sends match a subscriber by `subscriberExternalId`
when provided, then by recipient email. Saved first and last names fill
missing name variables automatically. Explicit request variables remain
authoritative. Multi-recipient sends do not use one subscriber's name for the
whole message.
### Custom Attributes
Any subscriber custom attributes are available:
```html theme={null}
Your plan: {{plan}}
Account ID: {{userId}}
```
### Passed Variables
Variables passed in the API request:
```json theme={null}
{
"variables": {
"orderNumber": "ORD-123",
"total": "$99.99"
}
}
```
```html theme={null}
Order: {{orderNumber}}
Total: {{total}}
```
## API Reference
### Send Email
```bash theme={null}
POST /api/v1/transactional/send
```
**Template mode:**
```json theme={null}
{
"to": "user@example.com",
"slug": "template-slug",
"variables": {
"key": "value"
}
}
```
**Direct mode:**
```json theme={null}
{
"to": "user@example.com",
"subject": "Email Subject",
"body": "Email content",
"variables": {
"key": "value"
}
}
```
The REST API keeps `slug` and `body` as the canonical fields. For compatibility
with MCP-style callers, `templateId` is accepted as an alias for the
transactional API slug and `html` is accepted as an alias for `body`. If both a
canonical field and its alias are provided, their values must match.
### Marketing delivery policy
The endpoint defaults to `"emailType": "transactional"`. For a consented
lifecycle or promotional message, set `"emailType": "marketing"`. Marketing
mode supports one recipient with no CC/BCC, creates or links a minimal
subscriber, honors unsubscribe suppression, adds the standard marketing
footer, and emits RFC 8058 one-click-unsubscribe headers automatically:
```json theme={null}
{
"to": "user@example.com",
"slug": "trial-reminder",
"emailType": "marketing",
"variables": {
"firstName": "Ana"
}
}
```
The caller is responsible for consent or another lawful basis. Do not pass an
unsubscribe URL; Sequenzy generates recipient-specific signed URLs.
### Custom headers and your own unsubscribe link
Add extra email headers with the `headers` object. On a transactional send you
can keep your own per-email-type unsubscribe by passing your one-click URL:
```json theme={null}
{
"to": "user@example.com",
"slug": "weekly-digest",
"headers": {
"List-Unsubscribe": "",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
}
}
```
Unsubscribes through your link go to your system, so Sequenzy does not suppress
the address. Marketing sends always carry Sequenzy's signed unsubscribe headers
instead of yours. Headers Sequenzy manages, such as `From`, `To`, `Subject` and
`Reply-To`, are never applied. A header that is not applied does not fail the
send; the response lists it in `ignoredHeaders` with a reason. See
[Send Email](../api-reference/transactional/send) for the full rules.
The response echoes the accepted `emailType`. If the email includes
`{{viewInBrowserUrl}}` (or `{{VIEW_IN_BROWSER_URL}}`), Sequenzy replaces it
with a hosted copy URL. Without a reply identity override, saved-template sends
prefer the template's reply profile. Otherwise, sends use the effective sending
domain's default reply profile, then the company default, then the first company
reply profile. The resolved value is retained with the send record.
**Response:**
```json theme={null}
{
"success": true,
"emailSendId": "send_abc123",
"emailType": "marketing",
"jobId": "job_abc123",
"to": "user@example.com"
}
```
Use the durable `emailSendId` with
`GET /api/v1/email-sends/{emailSendId}` to read one message's delivery status,
open/click timestamps, and event timeline. For aggregate Send API rates, call
`GET /api/v1/metrics?period=30d&emailType=transactional` or run
`sequenzy stats --email-type transactional --period 30d`.
Repeated identical transactional content sent to many distinct recipients can
trigger a junk/list-testing review. Sending continues while that review is pending
or unavailable; a substantiated junk verdict can make later matching deliveries
fail before sending. A rejected delivery is not held for automatic replay, and
retrying it from the dashboard also fails without sending. Check its
details and correct the content or contact support before deliberately sending
again. See [rejection and retry behavior](/api-reference/transactional/send#repeated-junk-content-rejection).
To discover recent deliveries by subject/title, recipient, or status, call
`GET /api/v1/email-sends?search=trial-reminder&status=opened` or run
`sequenzy email-sends list --search trial-reminder --status opened`. For one
saved template's aggregate rates, call
`GET /api/v1/metrics/transactional/{idOrSlug}` or run
`sequenzy stats --transactional `. The template-level result also
includes top clicked links, complaints, replies, latest bounce classifications,
and separate human/machine engagement counts.
### List Templates
```bash theme={null}
GET /api/v1/transactional?search=order&status=active&sort=open-rate&order=desc
```
**Response:**
```json theme={null}
{
"success": true,
"transactional": [
{
"id": "trans_abc123",
"slug": "order-confirmation",
"name": "Order Confirmation",
"emailId": "email_abc123",
"subject": "Your order is confirmed",
"enabled": true,
"stats": {
"sends": 120,
"deliveries": 118,
"opens": 92,
"clicks": 40,
"bounces": 2,
"openRate": 77.97,
"clickRate": 33.9
}
}
]
}
```
### Get Template Details
```bash theme={null}
GET /api/v1/transactional/:idOrSlug
```
**Response:**
```json theme={null}
{
"success": true,
"transactional": {
"id": "trans_abc123",
"slug": "order-confirmation",
"name": "Order Confirmation",
"emailId": "email_abc123",
"subject": "Your Order #{{orderNumber}}",
"blocks": [],
"enabled": true,
"variables": ["orderNumber", "total", "items"]
}
}
```
### Update Template
```bash theme={null}
PATCH /api/v1/transactional/:idOrSlug
```
Partial update: only the fields you send change. Pass `name`, `enabled`, `subject`, or `previewText` (use `null` to clear it) for metadata, and either `html` or `blocks` to replace the body.
```json theme={null}
{ "enabled": false }
```
Setting `enabled` to `false` is how you stop a template from sending while keeping it, its slug, and its stats. Sends to a disabled slug are rejected.
From the CLI, this is `sequenzy transactional update `, which takes the same fields as flags (`--name`, `--subject`, `--preview-text`/`--clear-preview-text`, `--enabled`/`--no-enabled`, `--html`/`--html-file`, `--blocks-json`/`--blocks-file`). From MCP, it is `update_transactional_email`.
### Delete Template
```bash theme={null}
DELETE /api/v1/transactional/:idOrSlug
```
Retires a saved template and frees its slug for reuse. Requires an API key with the `transactional:delete` scope.
**Response:**
```json theme={null}
{
"success": true,
"message": "Transactional email deleted",
"deleted": {
"id": "trans_abc123",
"slug": "order-confirmation",
"name": "Order Confirmation",
"emailId": "email_abc123"
}
}
```
The email content itself is kept as a reusable template, the same way deleting a campaign leaves its email behind. The returned `emailId` is what you pass to `DELETE /api/v1/templates/{templateId}` if you want the content gone too.
Past deliveries are unaffected - send history and stats live on the deliveries themselves - so analytics for a deleted template's previous sends stay intact. To stop sends without losing the template, [update](#update-template) it with `enabled` set to `false` instead.
You can also delete from the CLI with `sequenzy transactional delete --yes`, from MCP with `delete_transactional_email`, or from the dashboard.
## Auto-Creation
Transactional sends do not require a subscriber. When the recipient matches an
existing subscriber, the send is linked to them, so it appears in their
activity. Sends with `emailType: "marketing"` create or link a minimal
subscriber and honor unsubscribes; see
[Send Transactional Email](/api-reference/transactional/send).
## Error Handling
### Common Errors
| Error | Cause | Solution |
| - | - | - |
| `Template not found` | Invalid slug | Check slug spelling |
| `Template disabled` | Template is deactivated | Enable in dashboard |
| `Missing required fields` | No `to`, `slug`/`subject` | Include required fields |
| `No sender configured` | No sender profile | Set up sender in dashboard |
| `Sending paused` | Account sending is paused | Check sending status |
### Response Codes
| Code | Description |
| - | - |
| `200` | Email queued successfully |
| `400` | Validation error |
| `401` | Invalid API key |
| `404` | Template not found |
| `500` | Server error |
## Best Practices
### 1. Use Templates
Templates are easier to maintain and update:
```
✓ Template: Update once, affects all future sends
✗ Direct: Must update every code path that sends email
```
### 2. Handle Variables Gracefully
Always provide defaults for optional variables, or guard larger optional
sections with conditionals:
```html theme={null}
Hi {{FIRST_NAME|there}},
Hi {{ firstName | default: "there" }},
{{#if subscriber.plan}}
Your {{subscriber.plan}} workspace is ready.
{{else}}
Your workspace is ready.
{{/if}}
```
Conditional sections support `if`, `unless`, and `else`. They do not support
comparisons or custom helpers.
### 3. Log Email Send IDs
Save the returned `emailSendId`. It is the durable ID for
[delivery status](/api-reference/email-sends/get):
```javascript theme={null}
const response = await client.transactional.send({
to: user.email,
slug: "order-confirmation",
variables: { orderNumber: order.id },
});
await saveToDatabase({
orderId: order.id,
emailSendId: response.emailSendId,
});
```
### 4. Use Meaningful Slugs
```
✓ "order-confirmation"
✓ "password-reset"
✓ "trial-expiring-reminder"
✗ "email1"
✗ "template_2024_01"
```
### 5. Test in Development
Use test emails before production:
```javascript theme={null}
const to =
process.env.NODE_ENV === "development" ? "test@yourdomain.com" : user.email;
```
## Integration Examples
### Order Confirmation
```javascript theme={null}
async function sendOrderConfirmation(order) {
await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: order.customerEmail,
slug: "order-confirmation",
variables: {
orderNumber: order.id,
total: formatCurrency(order.total),
items: order.items.map((i) => i.name).join(", "),
shippingAddress: formatAddress(order.shippingAddress),
estimatedDelivery: formatDate(order.estimatedDelivery),
},
}),
});
}
```
### Password Reset
```javascript theme={null}
async function sendPasswordReset(email, resetToken) {
const resetUrl = `${APP_URL}/reset-password?token=${resetToken}`;
await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: email,
slug: "password-reset",
variables: {
resetUrl,
expiresIn: "24 hours",
},
}),
});
}
```
### Welcome Email
```javascript theme={null}
async function sendWelcome(user) {
await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: user.email,
slug: "welcome",
variables: {
firstName: user.firstName,
dashboardUrl: `${APP_URL}/dashboard`,
docsUrl: `${APP_URL}/docs`,
},
}),
});
}
```
## Organize templates with labels
Use **Labels** in a transactional email's row menu to assign existing company
labels or create new ones. Assigned labels appear below the email subject.
Filter the list by labels alongside search and active status. Selecting several
labels matches any of them; the filter stays in the URL when you reload or share
it. Duplicated transactional emails keep their labels, and deleting a company
label removes its assignments.
The public API accepts `labels` on [create](../api-reference/transactional/create)
and [update](../api-reference/transactional/update). Send `[]` to clear labels,
or omit the field to keep them unchanged on update. Read labels with
[get](../api-reference/transactional/get) or [list](../api-reference/transactional/list),
and filter with `?label=Billing`.
Use `sequenzy transactional create` or `update` with `--label Billing Receipts`,
`update --clear-labels` to remove assignments, and `list --label Billing` to filter.
MCP uses `labels` on `create_transactional_email`, `update_transactional_email`,
and `list_transactional_emails`. Label changes do not send emails.
## Related
Full API documentation
Auto-creation and attributes
Broadcast marketing emails
Automated email workflows
# UTM Tracking
Source: https://docs.sequenzy.com/concepts/utm-tracking
Automatically append UTM parameters to email links for consistent attribution
Sequenzy can automatically append UTM parameters to links in your emails so visits show up consistently in tools like Google Analytics, Plausible, PostHog, and Mixpanel.
Instead of manually tagging every button and link, you define a template once in **Settings → Tracking → UTM Tracking** and Sequenzy applies it during email rendering.
## What Gets Tagged
Auto UTM tagging applies to links generated in email content, including:
* Rich text links in text and HTML blocks
* Button blocks
* CTA blocks
* Clickable image blocks
* Product block links
UTM parameters are added when the final email HTML is generated, not only in
the editor preview. This keeps previews and sent emails aligned.
## What Does Not Get Tagged
Sequenzy intentionally skips links that should stay clean:
* Unsubscribe and preferences links
* Privacy policy and terms links
* Social profile links
* `mailto:`, `tel:`, and anchor links
* Any link manually marked to skip auto UTM tagging
This prevents polluted analytics and avoids breaking compliance-related links.
## Configure UTM Templates
Each field is optional. Only filled fields are appended.
| UTM field | Typical value | Common use |
| - | - | - |
| `utm_source` | `newsletter` | Identify the sender or channel |
| `utm_medium` | `email` | Keep email traffic grouped consistently |
| `utm_campaign` | `{{email.subject}}` | Tie visits to a campaign or email topic |
| `utm_content` | `{{link.text}}` | Distinguish multiple links in one email |
| `utm_term` | `variant-a` | Track experiments or optional dimensions |
Default settings:
```text theme={null}
utm_source=email
utm_medium=email
utm_campaign={{email.subject}}
utm_content={{link.text}}
utm_term=
```
## Available Template Variables
You can mix static text with variables:
| Variable | Resolves to |
| - | - |
| `{{company.name}}` | Company name |
| `{{company.websiteUrl}}` | Company website URL |
| `{{email.id}}` | Internal email ID when available |
| `{{email.subject}}` | Email subject |
| `{{link.host}}` | Destination hostname |
| `{{link.text}}` | Visible link text |
| `{{link.url}}` | Original destination URL before UTM tagging |
Example:
```text theme={null}
utm_source={{company.name}}
utm_medium=email
utm_campaign={{email.subject}}
utm_content={{link.text}}
```
## Per-Link Override
Sometimes you want one specific link to stay untouched, for example:
* A link that already has custom analytics parameters
* A hand-built attribution scheme for a partner campaign
* A legal or support link you do not want grouped into campaign traffic
In the email editor, open the link popover and switch that link to manual mode. Sequenzy will leave that URL unchanged while continuing to auto-tag the rest of the email.
## Example
Starting URL:
```text theme={null}
https://app.example.com/pricing?plan=pro
```
Settings:
```text theme={null}
utm_source=sequenzy
utm_medium=email
utm_campaign={{email.subject}}
utm_content={{link.text}}
```
Rendered link:
```text theme={null}
https://app.example.com/pricing?plan=pro&utm_source=sequenzy&utm_medium=email&utm_campaign=Launch+Week&utm_content=Start+trial
```
## Best Practices
* Keep `utm_medium=email` consistent across all email sends.
* Use a stable naming convention for `utm_source`.
* Use `utm_content` to compare hero buttons, inline links, and footer CTAs.
* Do not manually mix different naming conventions for the same campaign type.
* Review analytics naming quarterly so old and new campaigns still group cleanly.
## Related
Learn how opens, clicks, and unsubscribes are recorded
Analyze campaign and sequence performance
# Visual Blocks
Source: https://docs.sequenzy.com/concepts/visual-blocks
Design richer emails with hero, testimonial, gallery, badge, table, features, image card, and header blocks
Sequenzy's email editor includes a set of visual-first blocks that make campaigns and sequence emails look designed rather than plain. You can insert them from the block menu, and the AI email writer uses them automatically when it generates or improves emails.
This rendered example uses the same clean typography, coral accent, restrained surfaces, and direct SaaS copy as sequenzy.com. A header, hero, plain feature row, and testimonial read as one email rather than a catalog of unrelated blocks.
## Looking Up a Block's Fields
The pages below describe what each block is for. When you are writing blocks by hand - through the API, the CLI, or an AI agent over MCP - you can read the exact fields each type accepts:
```bash theme={null}
sequenzy blocks # every block type with its required fields
sequenzy blocks steps # full field reference for one type
```
The same reference is available at `GET /api/v1/email-blocks`, through the `get_email_block_schema` MCP tool, and as the `sequenzy://email-blocks` MCP resource. It is generated from the schemas that validate a write, so it always matches what is actually accepted. If the MCP tool is missing from a client's tool list, enable it on the Sequenzy connector, or read the resource or HTTP endpoint instead of inferring fields from live HTML.
Lists are the one shape worth knowing up front, because they are their own block type rather than a text variant:
```json theme={null}
{ "type": "list", "variant": "numbered", "items": [{ "content": "First" }] }
{ "type": "steps", "variant": "numbered", "items": [{ "title": "Step one", "description": "What happens" }, { "title": "Step two", "description": "What happens next" }] }
```
A `text` block accepts only `variant: "paragraph"`, `"lead"`, or `"html"`, and never accepts `items`. Use `list` for a plain numbered or bulleted list, and `steps` for a visual numbered walkthrough. List items carry `content`; steps items carry `title` and an optional `description`.
## Hero
The hero block overlays a headline, supporting text, and a call-to-action button on a full-width background image - the signature opening section of modern marketing emails.
* **Image variant**: your text sits on top of a background photo with an adjustable color scrim that keeps it readable. The hero runs edge-to-edge when full bleed is enabled.
* **Color variant**: the same layout on a solid color band, useful when you do not have a suitable image.
* **Settings**: eyebrow label, heading, subheading, button, overlay color and opacity, minimum height, and left or center content alignment.
Outlook for Windows cannot render background images, so recipients there see the same content on a solid band in your overlay color. Everything stays readable - no broken layouts.
## Testimonial
A styled quote block for social proof, with author name, role, an optional round avatar, and an optional 1-5 star rating rendered in your brand color.
* **Card**: quote on a softly tinted panel
* **Minimal**: editorial style with an accent border on the left
* **Centered**: a large centered quote with a decorative quote mark
* **Quote**: a serif pull-quote with a short accent rule and letterspaced attribution
* **Editorial**: a large serif quote set between thin hairline rules - the most magazine-like treatment, with no panel at all
## Gallery
A multi-image grid for lookbooks, product roundups, and event photos.
* **Two-up**: pairs of images per row
* **Three-up**: three images per row
* **Mosaic**: one large image with a row of smaller images below
Each image can link to its own URL, and you control the gap, corner radius, and row height.
## Badge
A small label pill for eyebrows and status tags, for example NEW, SALE, or CASE STUDY.
* **Pill**: filled with your brand color
* **Outline**: bordered
* **Soft**: tinted background with brand-colored text
## Table
A data table for pricing changes, plan comparisons, specs, and order summaries. You define up to six columns (each with its own left, center, or right alignment) and up to 30 rows of plain-text cells - merge tags work inside cells.
* **Header row**: shown by default on a tinted background, or hide it for simple grids
* **Zebra striping**: tint every other row for easier scanning on longer tables
## Features
A feature-highlight grid: icon, title, and description items arranged in one to three columns.
* **Cards**: each feature on a tinted tile
* **Plain**: centered text without panels
* **Icon left**: the icon sits beside each feature as a text row
Each item's icon can be an emoji or your own image. In the block settings, upload an image or paste an icon URL - an exported Lucide icon, a Flaticon PNG, or anything else hosted on a public URL - and it replaces the emoji for that item. **Icon size** sets the pixel size (16-64) for every custom icon in the block, so mixed sources still line up.
Use it for "what's included", "how it works" steps, or benefit summaries without building the layout by hand from columns.
## Image Card
An image and copy paired on a single card: picture, title, description, and an optional button.
* **Image top**: the image spans the card with content below, like a blog or product card
* **Image left / Image right**: the image sits beside the copy in a split layout
The image itself can link somewhere too: set **Image link** in the card settings and recipients who click the photo go to that URL, independently of the button.
## Image Shapes
Image blocks can be masked into organic shapes - arch, circle, leaf, petal, squircle, scallop, flower, and blob - for an editorial, Flodesk-style look. Pick a shape under **Style** in the image settings.
Shapes work in every email client, including Gmail and Outlook: the shape is baked into the delivered image server-side rather than applied with CSS, so recipients see exactly what you see in the editor. Any image with a fixed URL can be shaped - library uploads (including AI-generated images), Pexels and Unsplash stock photos, and other external images, which are automatically saved into your media library when you apply a shape so the delivered image can never change or disappear underneath the mask. Only dynamic image sources (merge tags, per-recipient product images) cannot be shaped, since they resolve at send time. A shaped animated GIF or WebP shows its first frame as a still image, so leave animations unshaped to keep them moving.
A shape replaces the frame and corner radius options for that image. Width, alignment, links, captions, and fixed-height cropping keep working as usual. When you write blocks through the API, CLI, or MCP, set the image block's optional `shape` field to one of `arch`, `ellipse`, `leaf`, `petal`, `squircle`, `scallop`, `flower`, or `blob`. Use a media-library, Pexels, or Unsplash URL on those surfaces; arbitrary external URLs are preserved unshaped until they are imported into the media library in the editor.
## Header
A header band for the top of your email, built around your company logo (the same logo your logo block uses).
* **Logo only**: just the logo, left-aligned or centered
* **Logo + button**: logo on the left, a brand-colored button on the right
* **Logo + nav**: logo on the left with up to four text links on the right, like a website masthead
## Button Styles
Buttons come in five styles so every email does not have to end in the same filled pill:
* **Fill**: your brand color as a solid button - the default
* **Outline**: a bordered button with brand-colored text
* **Pill**: a fully rounded filled button
* **Ink**: a near-black editorial fill that pairs with any brand color
* **Link**: a quiet letterspaced-caps text link with a trailing arrow, for secondary actions and soft section endings
### Setting button colors through the API
In the editor, the Button color and Text color pickers write to the block's
`buttonColor` and `buttonTextColor` fields. The separate Background control
writes to `styles.backgroundColor`, which is the band the block sits in rather
than the button itself.
That distinction matters when you write blocks through the API, CLI, or MCP:
| Goal | Field |
| - | - |
| Change the button | `buttonColor` |
| Change the label | `buttonTextColor` |
| Change the band | `styles.backgroundColor` |
Leave `buttonColor` and `buttonTextColor` unset to inherit your email theme's
primary color - that is why a button can keep its old color after you set
`styles.backgroundColor` on it.
Campaign, template, transactional, A/B test variant, and sequence email step
writes that set a field a block does not support still succeed, and the response
carries a `warnings` array naming the ignored field and the fields that block
does accept. The CLI prints those warnings, and MCP returns them in the tool
result.
A write that is rejected outright gets the same information as an error: the
block type it was validated against, that type's required and optional fields,
the shape of any item array involved, and every field you sent that the type
does not accept.
## Repeated Blocks Are Reported
Every block you send is stored. Nothing is removed for looking like a duplicate.
When a run of two or more blocks repeats the run immediately before it, campaign
and template writes add a note to the same `warnings` array, so a generator that
emitted the same section twice is easy to spot without the API deciding for you.
Block ids, styling, and whitespace differences are ignored when detecting a
repeat, so a section you deliberately repeated in a second colored band is
reported too - that is a note about your blocks, not a change to them. Sequence
email steps do not report repeated runs yet.
Blocks that come back from AI email generation are the exception: those are
de-duplicated before they reach a draft, because a model repeating the whole
body is a generation fault rather than a choice.
## Image Captions and Frames
Image blocks can carry a small muted **caption** line below the photo, and an optional frame treatment:
* **Border**: a thin hairline drawn directly around the image
* **Mat**: a white gallery mat inside a hairline border, like a framed print
Both make photography feel art-directed instead of pasted in.
## Numbered Headings
Turn on **Show heading number** in heading settings to add a number such as "01" beside your heading. The **Heading number** field appears only when enabled. Turn it off to return to an ordinary heading. The Numbered Heading component starts with this option enabled.
## Ready-Made Editorial Components
The editor's block menu (and the `/` slash menu) includes composed editorial pieces you can drop in as-is:
* **Framed Box**: a thick-bordered square section with a numbered heading - the brutalist "make one point well" panel
* **Numbered Heading**: a heading with an oversized "01" index already attached
* **Editorial Quote**: the serif pull-quote between hairline rules
* **Ledger**: a borderless label/value table for specs, editions, and statements
* **Dots** and **Accent Bar** dividers from the More blocks group
Two more looks are settings rather than blocks: the **Mat** frame on the
Image block adds the white gallery mat (captions are a separate Image field),
and the **Ink** and **Link** styles live in the Button block's settings.
## Divider Styles
The divider block now supports three styles: a classic **line** (with custom width and color), **dots** for editorial section breaks, and a short **accent** bar in your brand color that works well under section headings.
## AI Generation
When you generate an email with AI - in the campaign assistant or when AI writes sequence emails - it composes with these blocks automatically: opening with a header or hero, using badges as eyebrow labels, adding testimonials for social proof, laying out benefits with features grids, presenting pricing or specs as tables, and building galleries and image cards for product or photo content. Images come from your brand imagery, your product catalog, or a curated stock library, and the AI only uses real image URLs it was given.
The AI can also add [countdown timers](/concepts/countdown-timers) when an email has a real deadline, such as a sale ending or an event starting. Store sequences can render the recipient's own basket with the [cart, order, and free-shipping blocks](/concepts/cart-and-order-blocks).
All visual blocks follow your [email theme](/concepts/email-themes), so colors, corner radius, and typography stay consistent with the rest of your email.
# Website Tracking
Source: https://docs.sequenzy.com/concepts/web-tracking
Track on-site product views, cart activity, searches, and page views from any website with the Sequenzy browser SDK
Shopify stores get on-site tracking from the [storefront pixel](/integrations/shopify), installed for you when you connect the store. The **browser SDK** gives every other site the same events: custom storefronts, headless shops, marketplaces, booking and reservation sites, and SaaS marketing sites.
Once installed, these events arrive exactly like a native integration's, so the same automations work:
| Event | Fires when | Dashboard event | Powers |
| - | - | - | - |
| `product_viewed` | A visitor opens a product page | `ecommerce.product_viewed` | Browse abandonment, segments |
| `product_added_to_cart` | A visitor adds an item | `ecommerce.product_added_to_cart` | Cart abandonment |
| `product_removed_from_cart` | A visitor removes an item | none (updates the tracked cart) | Cart snapshot upkeep |
| `cart_viewed` | A visitor opens the cart | `ecommerce.cart_started` | Cart abandonment |
| `collection_viewed` | A visitor opens a category page | `ecommerce.collection_viewed` | Interest segments |
| `search_submitted` | A visitor searches | `ecommerce.search_submitted` | Intent segments |
| `page_viewed` (`page()`) | An identified visitor views a page you report | `web.page_viewed` | Page-intent triggers |
The SDK names are the wire format. Each identified event lands on the
contact's timeline as the built-in event shown above - the same
names you see under **Settings → Events & Sync Rules** and pick as sequence triggers. They
are not a separate event system, and they are unrelated to any [custom
events](/concepts/events) you define yourself. Sequenzy also derives
`ecommerce.cart_abandoned` and `ecommerce.browse_abandoned` from this
activity; your site never sends those directly.
## Where is the purchase event?
There is deliberately no `order_completed` in the browser SDK. A browser event
is not trustworthy proof of payment (anyone can fire one from the console),
and checkout often finishes where the snippet is not running - a hosted
payment page, another device, a retried card. Purchases instead come from
your backend: call [`POST /api/v1/orders`](/api-reference/orders/create) with
your secret API key whenever an order is placed.
That one call does everything the "order complete" event would:
* Fires `ecommerce.order_placed` on the contact, with revenue attributes
(`ltv`, `totalSpent`, `ordersCount`) updated for segmentation.
* Clears the tracked cart immediately, so a pending cart-abandonment check
finds nothing to email about.
* Stops in-flight recovery sequences: cart and checkout recovery triggers get
a default stop condition on `ecommerce.order_placed`, so remaining emails
are cancelled the moment the order arrives.
You never manage cart or order IDs for this - identity is the contact (via
`identify`), and the order is matched to whatever cart that contact had
tracked. See [Custom commerce](/concepts/custom-commerce) for the full
orders and checkouts flow, including `ecommerce.checkout_started` for
abandoned-checkout sequences.
## Anonymous visitors are not lost
Most people browse before they ever type an email address. The SDK keeps that history:
1. A visitor browses anonymously. Their events are held in their browser. The
SDK still pings the endpoint so the dashboard's `lastUsedAt` shows the
snippet is alive, but nothing is stored server-side for an anonymous
visitor - the server answers `queued: false`.
2. The visitor signs in or reaches checkout. Your backend mints a short-lived
identity token, and your site calls `sequenzy.identify(email, token)`.
3. The held events are replayed against that contact at their original timestamps.
So a shopper who browses three products and then signs in arrives with all three on their timeline, not just whatever they do afterwards.
Buffered history lives in the visitor's own browser, so it does not follow them to a different device or survive them clearing site data. If a different person identifies on the same browser, the buffer is discarded rather than replayed onto the wrong contact.
## Step 1: Create a tracking key
In the dashboard, go to **Settings → Integrations → Website tracking** and create a key. Or from the terminal:
```bash theme={null}
sequenzy web-tracking create \
--name "Storefront" \
--origins https://example.com,https://*.example.com
```
The key is **publishable**: it ships in your page source and anyone can read it.
That is by design, and it is why it can only append anonymous storefront events,
never read contacts or reach the rest of the API. An origin allowlist limits
browser misuse, but it is not identity proof because non-browser callers can
forge an `Origin` header. Identified events therefore also require a signed,
short-lived identity token minted by your backend.
A bare domain is read as `https`. `https://*.example.com` covers subdomains at
any depth but not the apex, so list `https://example.com` separately if you
need both. Add your development origin (for example `http://localhost:3000`)
while you are wiring things up.
## Step 2: Install the snippet
Paste the snippet from the dashboard or CLI into every page. Its first script
creates lightweight method stubs synchronously, so calls made while the SDK is
still downloading are queued rather than lost:
```html theme={null}
```
The snippet embeds both the key and your workspace id, so use it as printed rather than rebuilding it - a wrong workspace id fails silently.
You can call the normal API immediately, including before the async loader
finishes:
```html theme={null}
```
Pre-load calls run in order as soon as the SDK loads.
### Install from npm
If your site has a JavaScript build, install the side-effect-free package and
create the client explicitly:
```bash theme={null}
npm install @sequenzy/web-sdk
```
```typescript theme={null}
import { createBrowserClient } from "@sequenzy/web-sdk";
const sequenzy = createBrowserClient({
publicKey: "seq_pk_your_key",
companyId: "your_workspace_id",
});
```
## Step 3: Identify your visitors
Call `identify` as early as your backend has authenticated someone - on sign-in,
and again at checkout. First mint a token from your backend using a secret API
key with `commerce:write`, `automations:trigger`, and `subscribers:write` (the
last one because minting can create the contact). Pass the same publishable
key that is in your snippet:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/web-tracking-identities" \
-H "Authorization: Bearer YOUR_SECRET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"publicKey": "seq_pk_your_key",
"email": "buyer@example.com"
}'
```
Tokens last 24 hours by default (`ttlHours` accepts up to 720). Mint a fresh
one on each sign-in and at checkout rather than reaching for a long lifetime -
the token is a bearer proof, and a short expiry is what limits a leaked one.
Minting also establishes the email as a contact if it is not one yet, so
identified events always have a profile to land on. The contact is created
active with no list memberships and no automations triggered - enroll it
through your normal signup flows when you want more than event attribution.
Return only `identityToken` to that signed-in browser, then pass it with the
same normalized email:
```javascript theme={null}
sequenzy.identify("buyer@example.com", identityToken);
```
The token is bound to the workspace, tracking key, email, and expiry. A copied
publishable key or forged email cannot trigger sequences, segment changes, or
cart/browse recovery. Never expose the secret API key in browser code.
Call `reset()` on sign-out so the next person using that browser starts clean:
```javascript theme={null}
sequenzy.reset();
```
## Step 4: Track events
Each event has a convenience method:
```javascript theme={null}
sequenzy.viewedProduct({
providerProductId: "SKU-123",
title: "Protein Powder",
imageUrl: "https://cdn.example.com/protein.jpg",
url: "https://example.com/products/protein",
priceCents: 8850,
currency: "USD",
});
sequenzy.addedToCart({
providerProductId: "SKU-123",
title: "Protein Powder",
quantity: 2,
priceCents: 8850,
currency: "USD",
});
sequenzy.viewedCart({
cartId: "cart-1",
currency: "USD",
lineItems: [
{ providerProductId: "SKU-123", title: "Protein Powder", quantity: 2 },
],
});
sequenzy.viewedCollection({
providerCollectionId: "supplements",
title: "Supplements",
});
sequenzy.searched("protein");
```
`providerProductId` is your own product id. Use the same value you push to the [product catalog](/concepts/custom-commerce) so recommendations and product blocks in emails resolve to the right item.
Cart abandonment needs the cart contents, so include `quantity` on every cart
event. Without it, a recovery email cannot say what was left behind.
### Page views
Call `page()` on pages you want to segment on or trigger sequences from, such
as pricing, a plan comparison or a feature page:
```javascript theme={null}
sequenzy.page();
// Override what is reported, for example in a single-page app
sequenzy.page({ path: "/pricing/annual", title: "Annual pricing" });
```
It records `web.page_viewed` with `url`, `path` and `title` properties, taken
from the current document unless you pass them. Use it as a sequence trigger
with an event property filter on `path`, for example to start a flow when
someone views `/pricing`. Segment event filters count every `web.page_viewed`
event and cannot filter on `path`, so if you segment on page views ("viewed a
page at least 2 times in the last 7 days"), call `page()` only on the pages you
want to count.
If you installed the snippet before `page()` existed, copy the current snippet
from **Settings → Integrations → Website tracking** first. Older snippets have no `page` stub,
so calling `page()` before the SDK finishes loading throws an error.
Page views are sent only for visitors you have identified. Anonymous page views
are dropped rather than held for replay, so they cannot crowd product and cart
activity out of the visitor's buffer. Every page view is stored on the contact's
timeline, so call `page()` on the pages that matter to your campaigns rather
than on every page of a large site.
### When a cart disappears without telling us
The tracked cart is only as current as the events your site sends. Plenty of
sites empty a cart without ever firing a removal: a session-scoped cart timing
out, a reservation or inventory hold being released, the shopper finishing
checkout on another device, or the snippet simply not loading on a page.
So a tracked cart expires. After `expireAfterHours` (default 72) with no
activity, the cart is dropped instead of being emailed about, and a later add
starts a fresh cart rather than merging into the old items. That is what stops
a recovery email from listing things the shopper no longer has.
Sites using the browser SDK currently always use the default expiry.
`sequenzy shopify settings update --cart-expire-after-hours` applies per
connected Shopify store only - it has no effect on SDK-tracked carts.
The cleanest fix is still to tell us directly - call
`sequenzy.viewedCart({ ... })` with the current contents whenever your site
knows them, or fire `removedFromCart` when the cart empties. An explicit cart
view replaces the whole snapshot, so it corrects any drift in one call -
including `sequenzy.viewedCart({ lineItems: [] })`, which clears the tracked
cart so no abandonment email goes out for items the shopper no longer has.
## Push notifications
The same snippet and key power web push. After turning on web push in
**Settings → Push**, call `sequenzy.subscribeToPush()` from a button and serve
the one-line service worker described in
[Push notifications](./push-notifications#web-push). Identified visitors'
browsers are linked to their contact automatically.
## Configuration
Extra attributes on the script tag:
| Attribute | Default | Purpose |
| - | - | - |
| `data-sequenzy-site` | Page hostname | Distinguishes storefronts when one workspace covers several brands |
| `data-sequenzy-identity-ttl-days` | `30` | How long a browser keeps applying a stored identity |
| `data-sequenzy-storage` | on | Set to `off` to keep everything in memory for the page load only |
The stored identity only applies while its identity token is also unexpired, so with the default 24-hour token the browser stays identified for at most a day after the last `identify` call regardless of this setting. `data-sequenzy-identity-ttl-days` caps the window from the other side.
Lower `data-sequenzy-identity-ttl-days` on any site where one device is routinely shared between buyers rather than belonging to one of them - shared workstations, kiosks, in-store terminals, or any flow where people commonly buy on someone else's behalf. A stored identity means "this browser was last used by this person", not "this browser belongs to this person", and a long window on a shared machine can attribute one person's browsing to another.
Setting `data-sequenzy-storage="off"` disables the anonymous buffer as well, so visitors who identify later arrive with no prior history. Use it only where a consent policy requires it.
## Server-side alternative
If your visitors are signed in, you do not need the browser at all. Post the same events from your backend with your secret API key:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/webhooks/commerce/api/YOUR_COMPANY_ID/customer-events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventName": "product_added_to_cart",
"email": "buyer@example.com",
"product": { "providerProductId": "SKU-123", "title": "Protein Powder" },
"cartLine": { "providerProductId": "SKU-123", "quantity": 2 }
}'
```
This is often the fastest way to get cart recovery live: it covers every signed-in shopper with no front-end work. The browser SDK then adds the visitors who browse before they sign in.
## Managing keys
```bash CLI theme={null}
sequenzy web-tracking list
sequenzy web-tracking get wtk_123
sequenzy web-tracking update wtk_123 --origins https://example.com
sequenzy web-tracking update wtk_123 --deactivate
sequenzy web-tracking delete wtk_123 --yes
```
```bash API theme={null}
curl "https://api.sequenzy.com/api/v1/web-tracking-keys" \
-H "Authorization: Bearer YOUR_API_KEY"
```
MCP exposes the same operations as `list_web_tracking_keys`, `get_web_tracking_key`, `create_web_tracking_key`, `update_web_tracking_key`, and `delete_web_tracking_key`. See the [MCP reference](/concepts/mcp).
Revoking a key stops its events within about a minute while keeping the key value visible, so you can still find the matching snippet on your site. Delete it once the snippet is gone.
## Troubleshooting
**No events arriving.** Check the key's `lastUsedAt`. If it is `null`, no event has successfully authenticated yet: the snippet may be missing or not loading, the page may not have fired an instrumented event, or the origin allowlist may be rejecting the request. Open your site's network tab and look for a POST to `/customer-events` and its response status.
**Events rejected with 403.** Either the page's origin is not on the key's
allowlist, or an identified event has a missing, expired, or mismatched identity
token. Note that `https://example.com` and `https://www.example.com` are
different origins, as are `http` and `https`.
**Anonymous history not appearing.** Confirm token minting succeeds and
`sequenzy.identify(email, identityToken)` runs at sign-in and checkout. Without
a valid proof, events stay anonymous and are never attached to a contact.
**Nothing works in an ad-blocked browser.** Some blockers drop third-party tracking scripts. Those visitors are covered by the server-side approach above if they sign in.
# WooCommerce Integration
Source: https://docs.sequenzy.com/concepts/woocommerce-plugin
Connect a WooCommerce store to Sequenzy to sync products, customers, orders, back-in-stock requests, and recovery events.
You can connect a WooCommerce store to Sequenzy with the Sequenzy WooCommerce plugin.
Once connected, Sequenzy can sync store data, track customer activity, and power WooCommerce recovery flows.
The plugin is available on WordPress.org: [Sequenzy for WooCommerce](https://wordpress.org/plugins/sequenzy-for-woocommerce/).
The Integrations page lists WooCommerce under E-Commerce. Open that row to install, reconnect, or inspect the plugin connection.
## What It Does
* Sync products, customers, guest buyers, and orders
* Deliver cart and checkout activity to Sequenzy
* Capture product back-in-stock requests from your storefront
* Trigger replenishment reminders after WooCommerce purchases
* Let you run a manual store sync from **Settings → Products**
* Show plugin connection health in Sequenzy
## How To Connect
1. Open **Settings → Integrations** in Sequenzy.
2. Find `WooCommerce` and click `Install Plugin`.
3. Open the WordPress.org plugin page for [Sequenzy for WooCommerce](https://wordpress.org/plugins/sequenzy-for-woocommerce/).
4. In WordPress, go to `Plugins -> Add Plugin`, search for `Sequenzy for WooCommerce`, install it, and activate it.
5. Open `WooCommerce -> Sequenzy` in WordPress.
6. Click `Connect to Sequenzy`.
7. Sign in to Sequenzy, choose the company, and approve the connection.
8. Return to WordPress and finish the connection if the plugin asks you to confirm.
You need owner or admin access to the selected company to approve the connection.
## What Syncs
### Store Data
* Products
* Registered customers
* Guest buyers
* Orders
### Recovery Events
* `ecommerce.cart_started`
* `ecommerce.cart_cleared`
* `ecommerce.checkout_started`
### Order Events
WooCommerce orders trigger `ecommerce.order_placed` with product line items:
```json theme={null}
{
"orderId": "501",
"orderedAt": "2026-04-02T00:00:00.000Z",
"lineItems": [
{
"provider": "woocommerce",
"providerProductId": "101",
"providerVariantId": "102",
"title": "Coffee Beans",
"quantity": 1,
"priceCents": 2500
}
]
}
```
Sequenzy uses `lineItems[].providerProductId` and `lineItems[].providerVariantId` for product-aware automation matching.
## Replenishment Reminders
WooCommerce products can use the same replenishment flow as Shopify products:
1. Sync your store products.
2. Open **Settings → Products**.
3. Choose a product.
4. Enable replenishment and set how many days after purchase the product usually runs out.
5. Choose whether matching should use the product or exact variant.
When a matching WooCommerce order is received, Sequenzy queues a reminder. If the customer buys the same product again before the reminder is due, the reminder is skipped. When the reminder fires, it triggers `ecommerce.replenishment_due`.
Sequences started by `ecommerce.replenishment_due` default to stopping when the same product is purchased again.
## Back-In-Stock Requests
WooCommerce can use the same back-in-stock waitlist model as Shopify. When a shopper asks to be notified, send the request to Sequenzy:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/woocommerce/back-in-stock/subscribe" \
-H "Content-Type: application/json" \
-H "Origin: https://store.example.com" \
-d '{
"storeUrl": "https://store.example.com",
"email": "buyer@example.com",
"providerProductId": "501",
"productTitle": "Coffee Refill Pack"
}'
```
For simple WooCommerce products, Sequenzy uses the product ID as the back-in-stock variant identity. If you have a variant-specific implementation, include `providerVariantId` and `variantTitle`. Variation stock webhooks should include the parent product ID as `parent_id` and the variation ID as `id`.
When a WooCommerce product update reports the product as in stock, Sequenzy marks matching active waitlist entries as notified and triggers `ecommerce.back_in_stock`.
```json theme={null}
{
"provider": "woocommerce",
"providerProductId": "501",
"providerVariantId": "501",
"productTitle": "Coffee Refill Pack",
"productUrl": "https://store.example.com/product/coffee-refill-pack",
"imageUrl": "https://store.example.com/uploads/coffee.png",
"requestedAt": "2026-04-01T12:00:00.000Z",
"backInStockAt": "2026-04-02T08:00:00.000Z"
}
```
If the product goes out of stock again after notification, Sequenzy triggers `ecommerce.back_in_stock_out_of_stock` so matching back-in-stock sequences can stop safely.
## Which Lists Store Customers Join
WooCommerce does not have per-integration list targeting, so the customers it
creates - including the full backfill when you first connect the store - follow
your workspace [Default Lists](/concepts/subscribers#default-lists) setting.
Check that setting before connecting the store. If the default is a
lead-magnet, webinar, or event list, the backfill drops every past store
customer into it, and a later campaign aimed at that list reaches far more
people than opted in. Set the default to specific lists (or to no lists) first:
```bash theme={null}
sequenzy companies get comp_abc123 # read the current default
sequenzy companies update comp_abc123 --default-lists-none
```
Changing the default only affects later writes. A list that has already been
filled needs cleaning up with `sequenzy lists remove-subscribers` or
[remove subscribers from a list](/api-reference/lists/remove-subscribers).
## Subscriber Profiles
WooCommerce data appears on subscriber profiles:
* Purchased products from order line items
* Product images and URLs when the synced catalog has them
* Pending replenishment reminders
* Active back-in-stock waitlist entries
* Revenue attributes such as `ordersCount`, `totalSpent`, `ltv`, and `aov` when available
## Addresses That Cannot Receive Email
A store with years of history almost always carries a few email addresses on
domains that no longer exist - typically an old guest checkout at a company or
school that has since shut down. These do not stop the import.
Sequenzy still creates the profile so the customer's orders have something to
attach to, but marks it as bounced: it joins no lists, matches no sends, and
triggers no automations. Its revenue still counts toward your store's history
and toward segments built on `ordersCount`, `totalSpent`, or `ltv`.
You can review which addresses were affected after a sync:
* In **Settings → Integrations**, on the WooCommerce integration
* With `sequenzy integrations`, under `Unusable records`
* In the `lastSyncSkipped` field returned by the [list integrations](/api-reference/integrations/list) endpoint and the `list_integrations` MCP tool
Each entry records the address, why it failed, and whether it was `suppressed`
(imported, but sends are blocked) or `skipped` (not imported at all). Nothing
needs to be fixed on your side - editing historical orders is not required, and
Sequenzy never emails a suppressed address.
Orders and customers that carry no email address at all, or an address that is
not a valid email, are reported the same way with the reason `missing_email`.
These are usually phone or point-of-sale orders. Sequenzy has no way to contact
them, so they are counted but not imported - which is what explains a store
ending up with fewer contacts than orders.
## Manual Sync
You can click `Sync Store` in **Settings → Products** to request a fresh WooCommerce sync from Sequenzy.
## Managing The Integration
* The initial sync starts automatically after approval
* You can reconnect or disconnect the integration from **Settings → Integrations**
* Sequenzy shows whether the plugin is connected and when it was last seen
* Manual sync requests are supported for plugin-connected stores
## Current Beta Scope
* Product, customer, guest buyer, and order sync
* Cart and checkout event delivery
* Product-level back-in-stock waitlists
* Product-level and variant-level replenishment matching from order line items
* Manual sync from Sequenzy
* Connection health and diagnostics
## Current Limitations
* `ecommerce.cart_started` only works when WooCommerce already knows the shopper email
* Truly anonymous guest carts cannot be recovered until the shopper identifies themselves
* Built-in back-in-stock capture UI is not packaged in the plugin yet; use the subscribe endpoint from your product page or custom block
* No per-integration list targeting: synced customers follow the workspace [Default Lists](/concepts/subscribers#default-lists) setting rather than a list you pick for WooCommerce alone
# WordPress Integration
Source: https://docs.sequenzy.com/concepts/wordpress-plugin
Connect a WordPress site to Sequenzy to sync users, profile updates, comment opt-ins, and hosted forms.
You can connect a WordPress site to Sequenzy with the Sequenzy WordPress plugin.
Once connected, Sequenzy can sync WordPress users, receive profile updates, optionally capture comment opt-ins, and embed hosted signup forms.
The plugin is available on WordPress.org: [Sequenzy Email Marketing](https://wordpress.org/plugins/sequenzy-email-marketing/).
The Integrations page lists WordPress under Websites & Forms. Open that row to install, reconnect, or inspect the plugin connection.
## What It Does
* Sync existing WordPress users during the initial connection
* Track new registrations and profile updates
* Optionally capture comment opt-ins
* Embed hosted signup forms with a WordPress shortcode
* Show plugin connection health in **Settings → Integrations**
## How To Connect
1. Open **Settings → Integrations** in Sequenzy.
2. Find `WordPress` and click `Install Plugin`.
3. Open the WordPress.org plugin page for [Sequenzy Email Marketing](https://wordpress.org/plugins/sequenzy-email-marketing/).
4. In WordPress, go to `Plugins -> Add Plugin`, search for `Sequenzy Email Marketing`, install it, and activate it.
5. Open **Settings → Sequenzy** in WordPress.
6. Click `Connect to Sequenzy`.
7. Sign in to Sequenzy, choose the company, and approve the connection.
8. Return to WordPress and finish the connection if the plugin asks you to confirm.
You need owner or admin access to the selected company to approve the connection.
## What Syncs
### Users
* Existing WordPress users during the initial sync
* New user registrations
* Profile updates for existing users
### Opt-Ins
* Optional comment opt-ins from WordPress comments
## Embedding Forms
Paste the shortcode into any WordPress page or post:
```text theme={null}
[sequenzy form="FORM_ID"]
```
Replace `FORM_ID` with the ID of your saved form (it starts with `form_`). The form editor's WordPress embed option offers a PHP snippet instead, which works without the plugin.
The shortcode renders the Sequenzy form and sends submissions directly to Sequenzy.
## Managing The Integration
* The initial sync starts automatically after approval
* You can reconnect or disconnect the integration from **Settings → Integrations**
* Sequenzy shows whether the plugin is connected and when it was last seen
## Current Beta Scope
* WordPress user sync
* Registration and profile update delivery
* Comment opt-in support
* Connection health and diagnostics
## Current Limitations
* No Gutenberg form block yet
* No automatic plugin updates yet
* WooCommerce behavior is part of the separate WooCommerce integration
# Workspaces, Billing, and Shared Limits
Source: https://docs.sequenzy.com/concepts/workspaces-and-billing
Understand billing ownership, pooled email usage, and sender-health isolation when one login has access to multiple company workspaces
Sequenzy separates **workspace access** from **billing ownership**. One login can
own several company workspaces and can also be a member of workspaces owned by
other people. Billing and account-level limits always follow the workspace
owner, not whichever member happens to be signed in or sends the email.
## Workspace Access
Use **Settings → Team** to invite teammates and review who can access the
selected company workspace. Roles control what a member can do inside that
workspace, but they do not change which owner account pays for the workspace or
whose pooled email allowance is used.
If you own two company workspaces, creating the second workspace does not
create a second core email plan. Both workspaces use your plan and the same
pooled email allowance.
## What Is Shared?
| Item | Scope | Multi-workspace behavior |
| - | - | - |
| Core email plan | Workspace owner account | One plan applies to every company owned by that account |
| Core plan billing | Workspace owner account | Another owned workspace does not create another core plan subscription or workspace-specific invoice |
| Monthly email allowance | Workspace owner account | Sends from all companies owned by the account count toward the same usage total; the allowance is not divided into workspace buckets |
| One-time email credits | Workspace owner account | Credits are spent by sends from any company the account owns |
| Core paid features | Workspace owner account | Owner-level benefits, such as removing Sequenzy branding, apply across the owner's companies |
| Daily sending safety cap | Company workspace | Each company has its own rolling sending cap |
| Bounce and complaint health | Company workspace | Metrics, warnings, and automatic sending pauses are evaluated separately for each company |
| SMS access and credits | Company workspace | SMS is enabled and consumed separately for each company |
| Subscribers, campaigns, sequences, integrations, and sender profiles | Company workspace | Data and configuration stay isolated in the selected company |
The ownership boundary matters when your login can access workspaces owned by
different people. A company you only joined uses **its owner's** plan and usage
pool. Sending from that company does not use the plan attached to companies you
personally own.
The owner-level monthly email allowance is an accounting and upgrade threshold,
not a separate hard gate for each workspace. Sequenzy continues recording usage
when the pooled allowance is exceeded. The hard operational protection is the
company-specific limit of 300,000 accepted emails in a rolling 24-hour window.
## Workspace Identity and Compliance
Each company workspace keeps its own public identity: company details, logo,
social links, and legal address. Keep these values current because Sequenzy uses
them in branded surfaces and required email footer content. These settings stay
with the selected company even when billing is shared with another workspace.
## Invoices and Add-ons
The core email plan is attached to the owner's billing account. Under normal
Stripe billing, owned workspaces therefore share one core plan subscription and
one core-plan invoice stream. A teammate with billing permission can manage that
subscription, but the teammate does not become the billing owner.
One login is not guaranteed to receive literally one invoice in every
configuration:
* A monthly SMS add-on can be billed as a separate subscription, while its
credits remain scoped to one company.
* Shopify-billed plans and add-ons appear through Shopify rather than as a
direct Stripe invoice from Sequenzy.
* Prorated upgrades or add-on changes can create an immediate billing document.
Adding another company workspace by itself does not create any of these extra
charges.
## Usage Example
Suppose you own Company A and Company B under one login:
* Your plan includes `N` emails for the current billing period, and you start
with `C` one-time email credits.
* Company A sends `A` emails and Company B sends `B` emails.
* Credits are consumed first, so your remaining combined allowance is
`max(0, N + C - A - B)`.
* A member who sends from Company B consumes the same owner-level pool.
If you are also an admin of Company C, which another person owns, sends from
Company C use that other owner's plan and usage pool.
## Enforcement and Reputation
Most deliverability enforcement is intentionally company-specific:
* Bounce and complaint thresholds use the selected company's sending history.
* A sender-health pause stops that company, not the owner's other companies.
* The daily safety cap is evaluated independently for each company.
* Subscriber unsubscribe state and complaint suppression remain inside the
company.
Some controls and infrastructure can still have a wider effect:
* A serious abuse finding can produce an **owner-account ban**. That blocks
sending for every company owned by that account and pauses verification work
for those companies' sending domains.
* Owner-level trust and safety status is shared across the owner's companies and
can affect safety review or recovery decisions.
* Permanent and undetermined recipient bounces are suppressed platform-wide so
another company does not retry a destination already known to be invalid.
* The default `sequenzymail.com` domain and shared sending pools have reputation
shared with other senders. A verified custom sending domain gives each brand
a separate domain identity.
* Dedicated IP inventory belongs to the billing owner. If the same dedicated IP
is assigned to domains from more than one owned company, those domains share
that IP's reputation.
* Amazon SES maintains a regional account-level suppression list. An exact
recipient on that provider list can be suppressed across companies using the
same SES account and region, even though Sequenzy's local suppression records
are company-specific.
For the strongest separation between brands, use a different verified sending
domain for each company. Use separate dedicated IPs as well when you need IP
reputation to be independent.
## MCP and API Context
With an account API key or MCP connection, `get_account` lists every accessible
company and your role in each one. Use `select_company` in MCP, or send
`x-company-id` with an account API key, before reading or changing workspace
data. Selecting a company changes the workspace context; it does not create a
separate billing account or quota.
## Related
* [Authentication and workspace selection](/authentication)
* [MCP integration](/concepts/mcp)
* [Sender health and automatic pauses](/guides/sender-health)
* [Dedicated IP assignment](/concepts/dedicated-ips)
# Data Pipeline Ingestion
Source: https://docs.sequenzy.com/guides/data-pipeline-ingestion
Ingest contacts and events from your data warehouse or pipeline (Airflow, Dagster, dbt, cron) at scale, with built-in deduplication
Using Snowflake, BigQuery, Redshift or Postgres? [Warehouse
Sync](/integrations/warehouse-sync) pulls contacts and events on a schedule
with no pipeline code: write a query, map its columns, done.
When you already run a pipeline, or your source is not a supported warehouse,
push data through the API instead. It is designed so an existing DAG (an
Airflow task, a Dagster job, a dbt post-hook, or a plain cron script) can push
contacts and events in bulk, re-run safely after failures, and never create
duplicates.
This guide covers the endpoints a pipeline needs, how deduplication works on
each of them, and how to structure a task that moves large volumes of data
within the API rate limits.
## Endpoints at a Glance
| Purpose | Endpoint | Batch size |
| - | - | - |
| Bulk import/update contacts | [POST /api/v1/subscribers/imports](/api-reference/subscribers/import-create) | 5,000 records |
| Check import progress | [GET /api/v1/subscribers/imports/\{importId}](/api-reference/subscribers/import-status) | - |
| Upsert a single contact | [POST /api/v1/subscribers](/api-reference/subscribers/create) | 1 record |
| Bulk record events, many contacts | [POST /api/v1/subscribers/events/imports](/api-reference/subscribers/events/import) | 25 events |
| Record an event | [POST /api/v1/subscribers/events](/api-reference/subscribers/events/trigger) | 1 event |
| Record many events (one contact) | [POST /api/v1/subscribers/events/bulk](/api-reference/subscribers/events/trigger-bulk) | 500 events |
| Bulk tag changes | [POST /api/v1/subscribers/bulk/tags/add](/api-reference/subscribers/tags/bulk-add) | - |
| Export metrics to your warehouse | [GET /api/v1/metrics](/api-reference/analytics/metrics) | - |
All endpoints use [API key authentication](/authentication) and share the
[rate limits](/api-reference/introduction#rate-limiting) of 300 requests per
minute and 20 requests per second per key.
## Why Re-Runs Are Safe
Every ingestion path deduplicates, so your retry strategy can simply be
"re-run the whole task":
* **Contacts** match on `email`, `externalId`, or `phone`. You choose what
happens on a match with `duplicateStrategy`: `skip` (default), `merge`, or
`overwrite`. Running the same import twice never creates a second contact.
* **Events** accept your own `eventId` as an idempotency key. A retried event
with the same ID for the same contact and event name is acknowledged with
`duplicate: true` and records nothing.
* **Historical contacts** are those whose rows in a request are all more than
an hour old. Their events appear in analytics and segments without firing
automations or double-opt-in mail. If any row for a contact is recent, that
contact's whole group is live; split live and historical extracts when you
need the silent path.
## Syncing Contacts at Scale
Use the [bulk import endpoint](/api-reference/subscribers/import-create) with
5,000-record batches. The standard limit of 300 requests per minute allows up to
1.5 million contact rows per minute, so request latency, not the limit, bounds a
sequential sync like the one below.
A typical incremental task:
1. Query your warehouse for rows changed since the last successful run (a
simple `updated_at > :watermark` works; you own the watermark).
2. Chunk the rows into batches of up to 5,000.
3. POST each batch with `duplicateStrategy: "merge"` so existing contacts are
updated in place.
4. Poll each returned import ID until `status` is `completed`, then advance
your watermark.
```python theme={null}
import time
import requests
API = "https://api.sequenzy.com/api/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
BATCH_SIZE = 5000
def sync_contacts(rows, run_id):
import_ids = []
i = 0
retries = 0
while i < len(rows):
batch = rows[i : i + BATCH_SIZE]
resp = requests.post(
f"{API}/subscribers/imports",
headers=HEADERS,
json={
"duplicateStrategy": "merge",
"optInMode": "confirmed",
# The key is scoped to the request content: retrying the same
# batch after a timeout replays the original import, while a
# corrected batch under the same key imports as a new one.
"idempotencyKey": f"{run_id}-batch-{i // BATCH_SIZE}",
"subscribers": [
{
"email": row["email"],
"externalId": row["customer_id"],
"firstName": row["first_name"],
"customAttributes": {
"plan": row["plan"],
"mrr": row["mrr"],
},
"createdAt": row["signed_up_at"],
}
for row in batch
],
},
timeout=60,
)
if resp.status_code in (429, 503):
retries += 1
if retries > 8:
resp.raise_for_status()
time.sleep(int(resp.headers.get("Retry-After", "5")))
continue # retry the same batch; the idempotency key makes it safe
resp.raise_for_status()
import_ids.append(resp.json()["import"]["id"])
i += BATCH_SIZE
retries = 0
return import_ids
```
Instead of polling, you can subscribe an
[outbound webhook](/integrations/outbound-webhooks) to
`subscriber_import.completed`. It has one stable logical event ID per import,
and transient enqueue failures are repaired automatically. HTTP delivery is
retried, so consumers should deduplicate by event ID as usual.
If you prefer polling, confirm every import finished cleanly before advancing
your watermark:
```python theme={null}
def wait_for_imports(import_ids, timeout_seconds=1800):
for import_id in import_ids:
deadline = time.monotonic() + timeout_seconds
while True:
data = requests.get(
f"{API}/subscribers/imports/{import_id}", headers=HEADERS, timeout=30
).json()["import"]
# Imports report status "running", "completed", or "blocked" -
# there is no "failed" status; row failures appear in failedCount
# on a completed import.
if data["status"] in ("completed", "blocked"):
break
if time.monotonic() > deadline:
raise TimeoutError(f"{import_id} still {data['status']}")
time.sleep(5)
if data["status"] == "blocked":
raise RuntimeError(f"{import_id} was blocked pending review")
if data.get("failedCount"):
raise RuntimeError(f"{import_id}: {data['failedReasons']}")
```
Counts on the status endpoint reconcile exactly: `addedCount + updatedCount +
skippedCount + failedCount == totalRows`. Inspect `skippedReasons` and
`failedReasons` before treating a run as successful - a `duplicate_csv_row`
skip means two rows in your own extract carried the same email, phone, or
external ID.
### Preserve Signup Dates
Pass each contact's original signup date in `createdAt`. Date-relative
segments and analytics then reflect real history instead of the day your
pipeline first ran. An existing contact's date only ever moves earlier, so
repeated syncs cannot corrupt it.
### Do Not Enroll Backfills in Automations
Bulk imports do not fire signup automations by default. Leave
`enrollInSequences` off for warehouse syncs unless you explicitly want
imported contacts to enter matching sequences.
## Recording Events at Scale
For behavioral data (purchases, product usage, lifecycle events), use the
[events import endpoint](/api-reference/subscribers/events/import): bounded
batches of up to 25 events per request, each carrying its own contact identity.
Every event must have a deterministic `eventId` derived from your source data - an order ID, a
warehouse row key, or a hash of the row - so re-running the task records
nothing twice:
```python theme={null}
EVENT_BATCH_SIZE = 25
def sync_events(rows):
i = 0
retries = 0
while i < len(rows):
batch = rows[i : i + EVENT_BATCH_SIZE]
resp = requests.post(
f"{API}/subscribers/events/imports",
headers=HEADERS,
json={
"events": [
{
# null identifiers are accepted as absent. An
# externalId-only row must already exist from contact sync.
"externalId": row["customer_id"],
"email": row["email"],
"name": "warehouse.purchase",
"eventId": row["order_id"],
"occurredAt": row["ordered_at"],
# Row data must live inside properties - keys outside
# the documented fields are ignored.
"properties": {"amount": row["amount"], "sku": row["sku"]},
}
for row in batch
],
},
timeout=120,
)
if resp.status_code == 429:
retries += 1
if retries > 8:
resp.raise_for_status()
time.sleep(int(resp.headers.get("Retry-After", "5")))
continue # retry the same batch; eventId deduplicates replays
resp.raise_for_status()
summary = resp.json()
if summary["failed"]:
raise RuntimeError(f"{summary['failed']} events failed: {summary['failures'][:5]}")
if summary["sideEffectFailed"]:
raise RuntimeError(
f"{summary['sideEffectFailed']} event receipts need downstream recovery: "
f"{summary['sideEffectFailures'][:5]}"
)
i += EVENT_BATCH_SIZE
retries = 0
```
At 25 events per request within the standard request limit, that allows up to
7,500 events per minute while keeping each synchronous request bounded. Only a
contact whose rows in the request are all more than an hour old uses the silent
historical path; any recent row makes that contact's whole group live. Separate
live and historical rows when a backfilled purchase must not trigger a
"thanks for your order" sequence. For single fresh events (a checkout
happening right now) where you do want automations, use the
[single-event endpoint](/api-reference/subscribers/events/trigger).
## Handling Rate Limits
When you exceed the limit the API returns `429 Too Many Requests` with a
`Retry-After` header. An overlapping subscriber-import request using the same
idempotency key can similarly return a retryable `503` while the first request
still owns its short enqueue lease. For pipeline work:
* Honor `Retry-After` on `429` and retryable `503` responses, then resend the same request. Idempotency makes the
resend safe even if the original was actually processed.
* Keep the task single-threaded or coarsely parallel. One worker sending
5,000-row import batches already saturates the useful throughput; extra
concurrency only spends the budget faster.
* Fail the task on anything other than `429` and re-run it from the top. You
do not need checkpoint bookkeeping for correctness - only for speed.
## Syncing Data Out
For the reverse direction, warehouse tables fed from Sequenzy, set up a
[data export](/integrations/data-exports). Sequenzy writes email events, SMS
events, custom events and a daily subscriber snapshot to your S3 or GCS bucket
every few minutes, ready to load into Snowflake, BigQuery or Redshift. Use
[outbound webhooks](/integrations/outbound-webhooks) when you need individual
events in real time, or the [metrics API](/api-reference/analytics/metrics)
for pre-aggregated numbers.
## Checklist
1. Store the API key in your pipeline's secret manager, never in DAG code.
2. Sync contacts with 5,000-record batches and `duplicateStrategy: "merge"`.
3. Pass an `idempotencyKey` per batch so timed-out requests can be resent.
4. Pass `externalId` on every record so identity survives email changes.
5. Pass `createdAt` (contacts) and `occurredAt` (events) from source data.
6. Derive `eventId` deterministically from your source rows.
7. Subscribe a webhook to `subscriber_import.completed`, or poll import
status, and reconcile counts before advancing your watermark.
8. On failure, re-run with the same IDs: receipts deduplicate and downstream
recovery is re-attempted idempotently.
# Implementing DMARC
Source: https://docs.sequenzy.com/guides/dmarc
Implement DMARC to build trust in your domain and protect against email spoofing and unauthorized use of your domain in email messages
DMARC helps protect your domain from being used in phishing attacks and improves email deliverability by giving mailbox providers confidence that your emails are legitimate.
## Prerequisites
Since DMARC relies on DKIM and SPF, first ensure your existing emails are passing these checks:
* **DKIM** verifies the email wasn't altered in transit using cryptographic authentication
* **SPF** authorizes IP addresses to send email for a domain
If you have a [verified domain](/guides/domain-verification) with Sequenzy, your emails are already passing SPF and DKIM.
## What is DMARC?
DMARC ([Domain-based Message Authentication, Reporting, and Conformance](https://dmarc.org/overview/)) is an email authentication protocol that instructs mail servers what to do if an email message fails SPF and DKIM, preventing email spoofing (forged headers). DMARC is added to a domain through a TXT record at `_dmarc`.
By preventing spoofing, a domain can build trust with mailbox providers, as it allows them to verify that emails are authorized to send on behalf of that domain.
An email must pass either SPF or DKIM checks (but not necessarily both) to achieve DMARC compliance and be considered authenticated. A message fails DMARC if both SPF and DKIM fail.
## Implementing DMARC
### 1. Add a TXT `_dmarc` Record
To start, add a flexible DMARC record to your domain:
| Name | Type | Value |
| - | - | - |
| `_dmarc.example.com` | TXT | `v=DMARC1; p=none;` |
This record specifies a few parameters (see [Reference](#reference) section for more details):
* `v` - **Version**: The version of DMARC
* `p` - **Policy**: Tells the inbox how to process messages that fail DMARC. Options are `none`, `quarantine`, `reject`. It's best practice to use `quarantine` or `reject`, but only do so once you know your messages are delivering and fully passing DMARC.
Once you've added a basic record, you should [set up DMARC reporting](#dmarc-reporting) to monitor your results.
To ensure you don't accidentally introduce breaking changes to your email
sending, we suggest starting with a policy of `p=none;` before moving to a
stricter policy.
### 2. Test to Confirm Delivery and Passing
To test emails, send an email from all the applications and services your domain uses. Confirm that the messages are delivered to the inbox and that the headers show DMARC passing.
Spend a few days at this step to ensure you're checking all sources of email from your domain and catch email that is sent at a different cadence than daily.
To confirm DMARC passed, you can inspect the email headers and confirm there is `dmarc=pass`.
Gradually identify email sources using tools like [Google Postmaster
Tools](https://gmail.com/postmaster/), which provides DKIM/SPF feedback.
[DMARC monitoring
services](https://dmarc.org/resources/products-and-services/) can aggregate
your email sources by collecting DMARC reports, helping you discover any
services sending email on your domain's behalf.
### 3. Upgrade Policy
Once you have verified DMARC is passing across all your sending, you should upgrade your policy to `p=quarantine;`. This policy gives mailbox providers greater confidence in your domain since your domain only allows authenticated email.
| Policy | Description |
| - | - |
| `p=none;` | Allow all email. Monitoring for DMARC failures only. |
| `p=quarantine;` | Send messages that fail DMARC to the spam folder |
| `p=reject;` | Bounce delivery of emails that fail DMARC |
Once your policy is `p=quarantine;` or `p=reject;`, you can explore setting up
BIMI (Brand Indicators for Message Identification), which can provide
established brands even greater sending credibility by displaying a logo as an
avatar in an email client.
## DMARC Reporting
DMARC includes a reporting mechanism via the `rua` parameter, which tells mailbox providers where to send aggregate reports about emails from your domain. These reports are XML files that show which IP addresses are sending email on your behalf and whether those emails pass SPF and DKIM.
### Why you need a monitoring service
Raw DMARC reports are XML files sent as email attachments: they're not meant to be read directly. To get value from DMARC reporting, you should use a free monitoring service that parses these reports into a readable dashboard.
### Recommended free services
* [EasyDMARC](https://easydmarc.com/): free tier with a dashboard that visualizes your DMARC data, shows sending sources, and highlights failures.
* [URIports](https://uriports.com/): free DMARC report monitoring with a clean dashboard and real-time alerts.
* [DMARC Analyzer (Mimecast)](https://www.dmarcanalyzer.com/): free analysis tools with detailed reporting.
### How to set it up
1. Sign up for a DMARC monitoring service
2. The service will give you a reporting address (e.g., `abc123@ag.easydmarc.com`)
3. Add `rua` to your DMARC record pointing to that address:
| Name | Type | Value |
| - | - | - |
| `_dmarc.example.com` | TXT | `v=DMARC1; p=none; rua=mailto:abc123@ag.easydmarc.com;` |
Don't point `rua` at your own email address (e.g., `dmarc@yourdomain.com`).
You'll receive raw XML files that are difficult to interpret without
specialized tooling.
Reports typically arrive daily. Give it a week or two to collect enough data before making policy decisions.
## Reference
While the DMARC protocol includes both `pct` and `ruf` parameters, they are
not widely followed by mailbox providers. These settings may not be respected
or followed.
| Parameter | Purpose | Example |
| - | - | - |
| `v` | Protocol version | `v=DMARC1` |
| `pct` | Percentage of messages subjected to filtering | `pct=20` |
| `ruf` | Reporting URI for forensic reports | `ruf=mailto:authfail@example.com` |
| `rua` | Reporting URI of aggregate reports | `rua=mailto:aggrep@example.com` |
| `p` | Policy for organizational domain | `p=quarantine` |
| `sp` | Policy for subdomains of the OD | `sp=reject` |
| `adkim` | Alignment mode for DKIM | `adkim=s` |
| `aspf` | Alignment mode for SPF | `aspf=r` |
## When Sequenzy Requires DMARC
Domains created with Sequenzy's unified DNS record set must have exactly one
valid `v=DMARC1` record before verification can complete. Sequenzy checks the
sending domain first and then its organizational domain, so a policy already
published for `example.com` can cover a sending subdomain such as
`mail.example.com`.
Existing domains with the legacy Amazon SES record set are not migrated and
keep DMARC as an advisory check. Their sending remains available if DMARC is
missing, but the dashboard continues to report the issue.
In both cohorts:
* DMARC applies to all mail from the domain, not only Sequenzy mail.
* Do not publish multiple DMARC records; multiple policies are invalid.
* Sequenzy accepts any valid policy. Start with `p=none` if you need to observe
all sending sources before moving to `quarantine` or `reject`.
## Related
Set up your sending domain with DNS records
Send triggered emails via API
# Domain Verification
Source: https://docs.sequenzy.com/guides/domain-verification
Set up your sending domain with DNS records for maximum email deliverability
Setting up a custom domain ensures your emails reach inboxes and aren't flagged
as spam. Sequenzy shows the exact records required for your domain. Publish
every record in that table: newer domains use a unified Sequenzy record set,
while existing domains keep their legacy Amazon SES record set.
You can start sending emails immediately using the built-in `sequenzymail.com`
domain without any DNS setup. However, this shared domain has shared
reputation across all users, so emails may land in spam. For best
deliverability, set up a custom domain.
## Why Domain Verification Matters
When you send an email, receiving mail servers (Gmail, Outlook, etc.) check if the email is legitimate. Without a verified custom domain, your emails may:
* Land in spam folders
* Be rejected entirely
* Damage your sending reputation
Domain verification proves:
1. **You own the domain** - Only someone with DNS access can add the required records
2. **You authorize Sequenzy to send on your behalf** - The records authorize
Sequenzy's sending infrastructure or, for a legacy domain, Amazon SES
It does not move your mailbox hosting to Sequenzy. The required DKIM,
`send.yourdomain.com`, and `inbound.yourdomain.com` records do not replace the
normal MX records that receive mail for `yourdomain.com`, so a mailbox hosted by
Hostinger, Google Workspace, or another provider can keep receiving mail.
After verification, an address or alias on the domain can be the visible From
address. Any valid external mailbox can also be Reply-To. Sequenzy still
performs outbound delivery through SES or Sequenzy's MTA; customer-managed
SMTP relays are not supported.
## How Email Authentication Works
Email authentication uses multiple protocols working together. When a receiving server gets your email:
1. **SPF Check** - "Is this server allowed to send for this domain?" → Looks up TXT record on bounce subdomain
2. **DKIM Check** - "Was this email modified in transit?" → Verifies cryptographic signature using public key from TXT record
DMARC is an additional layer that tells receiving servers what to do when
authentication fails. It is required for domains created with the unified
record set and remains advisory for existing legacy domains.
## Required DNS Records
Sequenzy shows the required DNS records for your domain during setup. The
values in the dashboard and API are authoritative; the examples below explain
the two supported record shapes.
### DKIM Record (1 TXT record)
DKIM adds a digital signature to every email. Sequenzy generates a unique key pair for your domain: outgoing emails are signed with the private key, and receiving servers verify using the public key published in this record.
| Type | Name | Value |
| - | - | - |
| TXT | `sequenzy._domainkey.yourdomain.com` | `v=DKIM1; k=rsa; p={base64key}` |
### Unified Record Set for New Domains
Unified domains keep the same customer DNS when Sequenzy changes the domain's
underlying sending transport:
| Type | Name | Example value |
| - | - | - |
| TXT | `sequenzy._domainkey.yourdomain.com` | `p={public-key}` |
| CNAME | `send.yourdomain.com` | `send.sequenzydns.com` (DNS only) |
| TXT | `_dmarc.yourdomain.com` | `v=DMARC1; p=none;` (or your current policy) |
| CNAME | `links.yourdomain.com` | `links1.sequenzydns.com` (DNS only, optional) |
The `send` CNAME is the bounce record. It points at a Sequenzy host that carries
the SPF and MX records for bounces, so it works even when your DNS host blocks
custom MX records, for example Namecheap while email forwarding is on. Sequenzy
checks SPF and MX at `send` through it. On Cloudflare, set the CNAME to DNS
only. A CNAME can't share its name with other records, so remove any other
records at `send` first.
Domains added before the single CNAME publish these two records at the same
name instead, and keep working as they are:
| Type | Name | Example value |
| - | - | - |
| TXT | `send.yourdomain.com` | `v=spf1 include:spf.sequenzydns.com ~all` |
| MX | `send.yourdomain.com` | `bounce.sequenzydns.com` (priority `10`) |
`send` is the default bounce subdomain. Under **Advanced options** when you add
a domain, or with `mailFromPrefix` in the API, you can choose another one. It is
set once, when the domain is added.
The CNAME is your company tracking domain, which every sending domain shares
(see [Custom Tracking Domains](#custom-tracking-domains)). It is listed with the
first domain you add, on that domain's root, and it is optional: the domain
verifies and sends without it. Once it works, later domains show it as already
set up.
The DMARC record may already exist at your organizational domain. Do not create
a second DMARC record: publish or keep one valid `v=DMARC1` record. Sequenzy
checks the sending domain first and then its organizational domain.
A unified domain may initially send through Amazon SES with DKIM alignment
while its `send.` bounce records remain ready for Sequenzy's MTA. This is
intentional; no customer DNS change is needed when an operator attaches or
detaches the domain from an MTA route.
### Legacy SES Record Set
Existing domains are not migrated automatically. Their dashboard may continue
to show:
| Type | Name | Example value |
| - | - | - |
| TXT | `sequenzy._domainkey.yourdomain.com` | `p={public-key}` |
| TXT | `send.yourdomain.com` | `v=spf1 include:amazonses.com ~all` |
| MX | `send.yourdomain.com` | `feedback-smtp.us-east-1.amazonses.com` |
| MX | `inbound.yourdomain.com` | `inbound-smtp.us-east-1.amazonaws.com` |
The inbound MX routes branded replies. Region-specific Amazon endpoints can
differ from these examples, so always use the stored value Sequenzy returns.
## DMARC Requirement
DMARC is required for the unified record cohort and advisory for legacy SES
domains. Any single valid DMARC policy passes the unified-domain gate;
Sequenzy suggests `v=DMARC1; p=none;` as a safe starting point but does not
force a particular `p=` policy.
Learn how to set up DMARC for your domain with step-by-step instructions
## Verification Methods
### Option 1: Automatic Setup with Domain Connect
If your DNS provider supports Domain Connect, you can set up supported record
sets from Sequenzy without copying values into your DNS dashboard manually.
Unified domains currently use manual setup or Cloudflare direct setup so the
required DMARC and Sequenzy MX/SPF values cannot be omitted by an older
Domain Connect template.
**Step 1: Start automatic setup**
1. Go to **Settings → Domain** and click your domain
2. On the setup page, choose **Add Automatically**. For Cloudflare, choose
**Sign in to Cloudflare**. If neither is offered, use manual setup below.
3. Sign in to your DNS provider if prompted
4. Review and approve the DNS changes
Sequenzy uses Domain Connect discovery to detect DNS providers that expose the standard `_domainconnect` record and support Sequenzy's template.
**Step 2: Return to Sequenzy**
After your DNS provider applies the records, it redirects you back to Sequenzy.
Verification starts automatically and can still take a few minutes while DNS
and the configured sending provider finish processing the changes.
Domain Connect does not give Sequenzy permanent access to your DNS account.
The provider approval page applies only the records shown for this setup.
## Custom Tracking Domains
Your company tracking domain gives tracked clicks and open pixels your branded
host, for example `links.yourdomain.com`. Every sending domain uses it, so you
add one CNAME once. It is available on every plan, including the free plan.
| Type | Name | Value |
| - | - | - |
| CNAME | `links.yourdomain.com` | `links1.sequenzydns.com` |
* Any subdomain you control works, even on a root you do not send from. A bare
domain such as `yourdomain.com` cannot hold a CNAME, so use a subdomain.
* Manage it in **Settings → Tracking**, or with the
[Tracking Domain API](../api-reference/tracking-domain/get).
* It never blocks sending. Until it verifies, and whenever its record is broken
or removed, links use Sequenzy's shared tracking domain. Brief DNS outages are
ignored; a verified domain has to fail for at least 30 minutes before links
move.
* If a tracking domain that was working stops working, the dashboard shows an
alert until you fix it.
* Changing to another tracking domain keeps links in emails you already sent
working, as long as you keep the old CNAME record. Removing it breaks them.
## Branded Reply Domains
Branded reply routing uses one MX record so trackable reply-to addresses can use
your domain, for example `reply+id@inbound.yourdomain.com`.
New sending-domain automatic setup includes this record. If you enable branded
replies later and the record is missing, **Settings → Tracking** shows a
separate automatic setup button when your DNS provider supports the Sequenzy
inbound-replies template.
| Type | Name | Value |
| - | - | - |
| MX | `inbound.yourdomain.com` | `inbound-smtp..amazonaws.com` |
## Landing Page Domains
Landing page custom domains use a CNAME record so published landing pages can
serve from your branded host, for example `pages.yourdomain.com`.
If your DNS provider supports Domain Connect and the Sequenzy landing-page
template, the Landing Pages workspace shows an automatic setup button for this
CNAME. If Vercel requires an ownership verification TXT record, Sequenzy uses a
second Domain Connect template that includes both records.
| Type | Name | Value |
| - | - | - |
| CNAME | `pages.yourdomain.com` | `pages.sequenzydns.com` |
| TXT | `_vercel.yourdomain.com` | `vc-domain-verify=...` |
### Option 2: Manual DNS Setup
1. Go to **Settings → Domain** and click your domain
2. Copy each DNS record from the verification table
3. Add them in your DNS provider's control panel
4. Return to Sequenzy and click **I've added the records**
**Common DNS Providers:**
| Provider | Where to Find DNS Settings |
| - | - |
| GoDaddy | My Products → Domains → DNS |
| Namecheap | Domain List → Manage → Advanced DNS |
| Route 53 | Hosted zones → Select domain → Create record |
| HostGator | Hosting → Launch cPanel → Zone Editor (see warning) |
HostGator has two DNS editors, and only one of them is live. If your domain
uses `nsXXXX.hostgator.com` hosting nameservers, records added under **Domains
→ your domain → DNS** in the customer portal are never published. Use
**Hosting → Launch cPanel → Zone Editor** instead. See [Records saved but
never detected](#records-saved-but-never-detected-wrong-dns-editor) below.
DNS propagation can take up to 48 hours, though most records propagate within
15-30 minutes.
## Verification Process
Once you've confirmed the DNS records were added, Sequenzy monitors their status:
1. **DNS Lookup** - Checks if records exist and point to correct values
2. **Home Transport Activation** - Sequenzy confirms the selected SES region or MTA route can accept mail
3. **DMARC Check** - Required for unified domains and advisory for legacy domains
4. **Inbound Routing** - When an inbound reply MX is present, Sequenzy connects the receipt rule for branded reply tracking
5. **Status Update** - DNS and sending readiness update independently
### Verification Statuses
The dashboard exposes three related states:
* **DNS** - Not started, pending, verified, misconfigured, or failed.
* **Sending** - Activating or ready to send. Ready requires verified DNS plus
the selected home transport.
* **SES region** - the selected AWS region whose identity must be ready.
A newly DNS-verified domain can briefly show **Activating SES** while AWS makes
the home-region identity sendable. Sequenzy retries automatically and does not
allow sends during that window. The domain becomes **Ready to send** as soon as
the selected home transport is ready.
A DNS-verified domain is never demoted because of a temporary DNS hiccup: a
status only changes after repeated, confirmed checks. While Sequenzy
double-checks an inconsistent answer, the domain shows a "Rechecking DNS
records" hint and keeps sending. If a verified domain does lose verification,
the dashboard shows exactly which record is missing or wrong so you can fix it,
and the domain returns to verified automatically on the next successful check.
## Troubleshooting
### Records Not Detected
* **Wait for propagation** - DNS changes can take up to 48 hours
* **Check for typos** - Ensure record names and values match exactly
* **Check proxy settings** - For Cloudflare, DKIM records must have proxy disabled (DNS only)
### Records Saved but Never Detected (Wrong DNS Editor)
If you added the records, they show in your provider's dashboard, but verification keeps reporting them as missing even after hours, you are probably editing a DNS zone that your domain does not actually use. Some providers keep two separate copies of your DNS: one attached to the domain registration and one attached to the hosting account. Only the copy served by your domain's active nameservers is live - changes to the other copy are saved but never published.
**How to check which zone is live:**
1. Find your domain's nameservers (in your provider's dashboard, or run `dig NS yourdomain.com`)
2. Make sure the DNS editor you are using belongs to the account or product those nameservers point to
3. After saving a record, verify it resolves publicly:
```bash theme={null}
dig TXT sequenzy._domainkey.mail.yourdomain.com
```
If this returns nothing several minutes after saving, the record is not being published - you are editing the wrong zone, not waiting on propagation.
**HostGator specifically:** HostGator shows a DNS editor in two places, and only one is live depending on your nameservers:
* If your domain uses hosting nameservers such as `ns8153.hostgator.com` (the default when you have a HostGator hosting plan), DNS is served from your hosting account. Add records via **Hosting → Launch cPanel → Zone Editor → Manage** next to your domain. TXT records are added from the **Add Record** dropdown on the Manage page.
* The **Domains → your domain → DNS** tab in the HostGator customer portal edits a registrar-side zone. When the domain points at hosting nameservers, records saved there are never published, even though the dashboard shows them as saved.
**cPanel refuses to save with a CNAME error:** cPanel validates the entire zone on save. If you see an error like `Only 1 "CNAME" record may exist per name` for a record you did not touch (often `cpanel.yourdomain.com` or `webmail.yourdomain.com`), the zone contains pre-existing duplicate records that block all changes. Filter the record list for that name, delete the duplicate CNAME row, then save your new records again.
### Verification Is Stuck
If verification stays pending or misconfigured:
1. Delete the domain in Sequenzy
2. Re-add it to get a fresh DKIM key
3. Update your DNS records with the new values
### Branded Replies Not Active
If `reply+{emailId}@inbound.yourdomain.com` is not active yet:
1. Confirm the inbound MX record points to the region-specific `inbound-smtp` endpoint shown in Sequenzy
2. Return to **Settings → Tracking** and refresh the Reply-to Domain status
3. Leave the Reply-to Domain setting on **Sequenzy** until inbound routing shows as active if you want to avoid fallback behavior
### Cloudflare Token Errors
| Error | Solution |
| - | - |
| "Invalid API token" | Check the token wasn't revoked or expired |
| "Could not access zone" | Ensure token has Zone - Zone - Read permission |
| "Failed to create record" | Ensure token has Zone - DNS - Edit permission |
## Using Subdomains
We strongly recommend sending from a subdomain (e.g., `mail.example.com`) rather than your root domain. This protects your domain reputation and isolates any deliverability issues.
Learn about reputation isolation, risk prevention, and best practices for
subdomain email sending
## Best Practices
1. **Use a subdomain** - Protect your root domain's reputation with `mail.yourdomain.com`
2. **Keep one valid DMARC record** - It is required for unified domains and protects legacy domains against spoofing
3. **Keep tokens secure** - Never share your Cloudflare API token publicly
4. **Verify inbound replies before switching** - Keep the Reply-to Domain setting on **Sequenzy** until the inbound MX record and routing status are active
## Related
Protect your domain reputation
Set up DMARC for your domain
Get started sending emails
Send triggered emails via API
# Email Editor
Source: https://docs.sequenzy.com/guides/email-editor
Build beautiful emails with the visual block editor and AI assistant
The Sequenzy email editor is a visual, block-based editor that makes it easy to create professional emails. You can drag and drop components, customize styling, and even use AI to generate or copy email designs.
## Editor Overview
The editor consists of three main areas:
1. **Canvas** - The central area where you build your email
2. **Components panel** - Left sidebar with blocks and ready-made sections
3. **Settings / AI** - Right sidebar for selected-block settings and the AI assistant
## Saving and discarding changes
**Discard changes** restores the last saved version. Its **Undo** action restores email content and expires when you edit, save, or navigate history. If you also changed settings such as styles, sender details, or attachments, you must confirm the reset because content Undo cannot restore those settings.
## Editing on smaller screens
On tablets, the AI assistant opens beside your email so you can keep editing. On phones, it opens as a modal panel; close it or press Escape to return to the editor. Your chat draft stays intact when you resize between phone and tablet layouts.
In Preview, use **Simulate** to switch devices, dark mode, and blocked images for both block and HTML emails. Phone controls also include preview data and test sends.
## Default Components
The email editor provides a comprehensive set of building blocks:
### Basics
The first group in the Components panel holds the blocks most emails use.
Click a block to insert it below the selected block, or drag it into place.
| Component | Description |
| - | - |
| **Heading 1** | Large title text for main headlines |
| **Heading 2** | Medium title for section headers |
| **Paragraph** | Standard body text |
| **Variable** | Personalizes content for each subscriber |
| **Image** | Upload or link to images |
| **Button** | Call to action with style settings |
| **Divider** | Horizontal line separator |
| **Spacer** | Adjustable vertical gap |
| **2 Columns** | Side-by-side layout |
### Sections
Ready-made sections (Structure, Content, Engagement, Shop, Commerce, Discounts
and Footer) are listed one per row directly under Basics, each with a short
summary of the blocks inside. Select a row to open a gallery of rendered
previews.
### More blocks
| Component | Description |
| - | - |
| **3 Columns** | Three-column layout |
| **Dots** | Editorial dotted divider |
| **Accent Bar** | Short brand-color bar |
| **Video** | Embed video with a poster image |
| **Logo** | Company logo with automatic sizing |
### Advanced
Advanced is expanded by default so logic and code blocks are immediately available.
| Component | Description |
| - | - |
| **Condition** | If/else branches shown to matching subscribers |
| **Repeat** | Repeats child blocks for each item in an array variable |
| **Lead Text** | Emphasized introductory text |
| **Code Block** | Multiline code |
| **HTML** | Custom email-safe HTML snippet |
### Engagement
The Engagement section holds testimonials, star ratings and the two feedback
blocks:
| Component | Description |
| - | - |
| **Testimonial** | Quote with author details |
| **Poll** | One-click answer buttons with up to eight options |
| **Rating** | Star rating display |
| **NPS Survey** | A 0-10 recommendation scale for measuring customer NPS |
Poll and NPS Survey save each recipient's latest answer to a subscriber
attribute and emit a `poll.answered` event for segmentation and automations.
Testimonial and Rating are static display blocks. See
[Polls & NPS Surveys](/concepts/polls) for setup, styling, results, and API
examples.
### Product Blocks
Product blocks can render a fixed product from your synced catalog or a dynamic product from merge-tag data.
Use synced products when the email should always show the same item. Use dynamic product data when the product comes from an event, such as a replenishment or back-in-stock sequence.
Dynamic product blocks can read values such as:
* `{{product.title}}`
* `{{product.imageUrl}}`
* `{{product.url}}`
* `{{product.price}}`
* `{{event.product.title}}`
* `{{event.product.imageUrl}}`
* `{{event.product.url}}`
* `{{event.product.price}}`
The preview data panel detects product variable groups, including event-scoped product groups like `event.product`, so you can preview the same email with different sample products before sending.
### Repeat Blocks
Use a **Repeat** block to render one group of child blocks per item in a list.
The block settings start with one question - **What to show** - with a single
choice per answer:
* **Recommended products** - picked automatically per recipient. Also
available as a one-click **Recommended products** block in the picker's
Commerce section. A **Ranking** setting chooses the strategy: personalized
for each recipient (browsing first, then similar shoppers, then
store-popular, then newest in stock), best sellers by recent order volume,
newest products, or the recipient's recently viewed items. Activity-based
strategies accept an **Activity window** (default 30 days). A product never
repeats within a list or across lists in the same email. You can optionally
narrow recommendations with catalog filters. See
[Product Recommendations](/concepts/product-recommendations) for the full
behavior and attribution stats.
* **Store catalog** - your synced catalog, sorted by newest, price, or title,
with optional filters by tag, collection, vendor, product type, or price
range, resolved at send time. Out-of-stock products are excluded by default.
Recommended and catalog lists share two fine-tuning toggles:
* **Skip already-purchased products** leaves out anything the recipient bought
in the last year. It is on by default for recommendations and off by default
for catalog lists.
* **Match the trigger event's product** only shows products related to the
product on the event that triggered the email - for example, a
browse-abandonment sequence recommending items related to the viewed
product. You choose which attributes must overlap: tags, collection,
product type, or brand, and whether products need to match **any** of the
selected attributes or **all** of them. The default is any shared tag,
collection, or type. The event's own product is never shown, and the toggle
is ignored for sends without a product event, such as campaigns.
The remaining choices:
* **Hand-picked products** - choose exactly which products to show and in what
order. Titles, images, prices, and stock stay synced with your store.
* **Campaign list** - a personalized list computed by the campaign's
personalization rules.
* **Custom variable** - point the block at an array in your data, such as
order items. A source path of `items` reads the `items` array from
transactional API variables and lets child blocks use scoped merge tags like
`{{item.title}}`, `{{item.description}}`, and `{{item.number}}`. Item
values are inserted as text, so HTML in a value shows as written. Use
`{{html.item.description}}` to insert an item's HTML, which is sanitized.
Product lists resolve at send time in campaigns, sequences, and test sends, so
filters also work inside automation emails. Child blocks read product fields
through the item alias: `{{product.title}}`, `{{product.imageUrl}}`,
`{{product.price}}`, `{{product.url}}`, and `{{product.buttonText}}`. The item
alias lives under **Advanced** in the block settings.
**Item templates.** The block settings include ready-made layouts - a card
grid, a single-product spotlight, compact thumbnail rows, and a minimal text
list - so you can restyle a repeated list in one click. Applying a template
replaces the blocks inside the list.
**Item style.** The **Item Style** section controls how each repeated item is
drawn: padding, background, corner radius, border, an optional fixed item
height, and a **Column content** toggle that aligns each child row across
items (**Distribute**). Turn on **Custom space between blocks** to set an
exact vertical gap between the blocks inside each item - set it to 0 to pack
a title, subtitle, and button flush together instead of using the theme's
default block spacing.
The preview data panel shows editable rows for variable-backed repeat sources.
Store-product lists preview with real products from your catalog instead.
### Structure and Content sections
The **Structure** section holds layout pieces: Header, Navigation, Hero, CTA,
Card, Feature, Image Card, Badge, Framed Box and Alert. Clicking a Header is
the one insertion exception: it goes to the top of the email (dragging one
places it where you drop it). The **Content** section holds editorial blocks:
Article, Quote, Editorial Quote, Numbered Heading, Ledger, List, Table,
Features, Steps, Stats, Pricing, Gallery, Product Grid, Author and Signature.
Author settings separate layout, author details and the optional link. Edit the name and role directly in the sidebar; Card also offers a short bio. Switching layouts preserves your saved photo and bio. Add both link text and a URL to display the author link.
### Footer
| Component | Description |
| - | - |
| **Social** | Social media icons (X, LinkedIn, Instagram, etc.) |
| **Simple** | Minimal footer with unsubscribe link |
| **Full** | Complete footer with address and links |
## Working with Blocks
### Adding Blocks
1. Select the block you want to build on, then click a block in the
Components panel
2. It appears right below your selection (or at the end of the email when
nothing is selected) and becomes the new selection, so the next click
continues below it. Blocks that open a dialog first (Image, products,
variables) land in the same place once you confirm. A clicked Header goes
to the top, and a block that cannot sit in that spot (such as a footer
inside a group) is added at the end instead
3. Drag a block from the panel instead to drop it exactly where you want it
### Editing Blocks
1. Click a block to select it
2. Edit text directly in the canvas
3. Use the settings icon for advanced options
### Formatting Text
Select any text in a text or heading block to open the formatting toolbar. From
there you can apply:
* **Bold** (⌘/Ctrl+B), **Italic** (⌘/Ctrl+I), and **Underline** (⌘/Ctrl+U)
* **Strikethrough** and **inline code**
* **Text color**, **links**, **bullet and numbered lists**, and **alignment**
#### Markdown Shortcuts
The editor recognizes common markdown as you type:
| Type | Result |
| - | - |
| `# `, `## `, `### ` | Heading 1, 2, or 3 |
| `- ` or `1. ` | Bullet or numbered list |
| `` `code` `` | Inline code |
#### Code
Use **inline code** for short fragments like coupon codes, variable names, or
short commands. Wrap the text in single backticks (`` `SAVE10` ``) or select it
and click **Code** in the toolbar.
For longer snippets, add a dedicated **Code block** from the `/` insert menu (or
the add-block button). It is a standalone block you can drag and reorder like
any other. Type your code directly into it across as many lines as you need -
indentation and line breaks are preserved exactly, and **Enter** simply adds a
new line.
##### Code themes
Give a code block a color theme to match your email's style. Open the block's
**settings** panel and pick from **Light** (the default), **Dark**,
**Midnight**, **Terminal**, or **Solarized**.
Themes use plain inline background and text colors (no syntax highlighting), so
they render consistently across email clients.
Code blocks render identically in the editor and the delivered email, so what
you see while editing is what your subscribers receive.
### Working With HTML Blocks
Use the **HTML** block when one section of your email needs custom markup.
1. Add the **HTML** block from the component panel or slash menu
2. Paste your snippet into the inline code editor
3. Review the live preview below the editor
4. Keep the HTML snippet-focused; use the full HTML editor only when the entire email is custom code
HTML blocks support merge tags like `{{ FIRST_NAME }}` and are rendered in the
same email pipeline as the rest of your blocks.
To replace the whole email with your own HTML instead, open the **HTML** tab
and choose **Import HTML**. You can upload a file or paste existing markup:
### Block Actions
When you select a block, you'll see action buttons:
* **Settings** - Shows all block controls in one panel. On wide screens it opens
automatically when you select a block, and the **Settings / AI** switch at the
top of the right panel highlights to show where the controls went. Use the
switch to move between block controls and the assistant. Settings is
unavailable until you select a block, and the panel returns to the assistant
when you deselect it
* **Duplicate** - Create a copy of the block
* **Delete** - Remove the block
Use **Choose image** to upload or pick an image, or paste an image URL. Your selected block and settings stay open while you use the picker. Link controls label the visible text and destination URL separately.
### Reusable Components
You can save your own reusable components from the same block editor.
1. Open a block's menu and choose **Save as component** to save that block for reuse
2. Click **Save section** in the Components sidebar to save several adjacent top-level blocks as one reusable component
3. Open **AI** in the Components sidebar if you want Sequenzy to generate a reusable section from a prompt
4. Insert saved components later from the sidebar, the inline add menu, or drag and drop
Saved components use the same email blocks as the main editor, so after you
insert one you can keep editing it like any other part of your email.
### Drag and Drop
Blocks can be reordered by:
1. Hovering over the block
2. Clicking the grip handle on the left
3. Dragging to the new position
## Fonts
Pick a system font or search the full Google Fonts catalog. The font applies to
your entire email. Set the default for new emails in **Settings → Email Design**.
### Selecting a Font
1. Open the **Style** tab next to **Components** in the editor sidebar
2. Click **Font** and search for a font or pick one from the list
3. Your email updates immediately
### System Fonts
These fonts are installed on most devices and render consistently across all email clients:
| Font | Style | Best for |
| - | - | - |
| System | Sans-serif | Clean, native look |
| Arial | Sans-serif | Universal compatibility |
| Helvetica | Sans-serif | Professional, modern |
| Georgia | Serif | Elegant, readable |
| Times New Roman | Serif | Traditional, formal |
| Verdana | Sans-serif | Highly readable |
| Trebuchet MS | Sans-serif | Friendly, casual |
| Tahoma | Sans-serif | Compact, clean |
| Courier New | Monospace | Code snippets, technical |
### Web Fonts
Any Google Font works. These are popular choices. They render in modern email clients (Gmail, Apple Mail, Outlook.com) with automatic fallbacks for older clients.
**Popular sans-serif fonts:**
* Inter, Roboto, Open Sans, Lato, Montserrat, Poppins
* DM Sans, Manrope, Plus Jakarta Sans, Space Grotesk
**Serif fonts:**
* Merriweather, Playfair Display, Lora, PT Serif
* Source Serif 4, Libre Baskerville, EB Garamond
**Display fonts:**
* Bebas Neue, Oswald, Anton
**Monospace fonts:**
* Roboto Mono, Fira Code, JetBrains Mono, Space Mono
Web fonts may not display in all email clients. Older versions of Outlook for
Windows use system font fallbacks. Always test your emails if exact font
rendering is critical.
### Font Fallbacks
All fonts include fallback stacks. If the primary font doesn't load, the email gracefully falls back:
```
"Inter" → System UI → Arial → sans-serif
"Playfair Display" → Times New Roman → serif
"Fira Code" → Courier New → monospace
```
This ensures your email remains readable regardless of the recipient's email client.
## AI Email Assistant
The assistant writes with the brand voice and product details you keep in
**Settings → AI Context**:
The AI assistant helps you create emails faster by generating content or copying designs from images.
### Generating Content
Type a prompt to generate email content:
* "Write a welcome email for new users"
* "Create a promotional email for our summer sale"
* "Make this email more engaging"
### Copying Email Designs
You can copy email designs from screenshots or images:
1. Click the attachment icon in the AI chat
2. Upload a screenshot of an email you want to recreate
3. Type "Copy this email design" or similar prompt
4. The AI will recreate the structure using Sequenzy blocks
When copying emails, the AI preserves the structure and layout while adapting
content to use your company information and personalization variables.
### Example Prompts
**Creating new emails:**
* "Write a 3-email welcome sequence"
* "Create an abandoned cart reminder"
* "Generate a product announcement email"
**Modifying existing content:**
* "Make the headline more compelling"
* "Add a testimonial section"
* "Shorten this email"
**Copying from images:**
* "Copy this email design" (with image attached)
* "Recreate this layout with my brand colors"
* "Use this structure but change the content to be about \[topic]"
## Personalization Variables
Use variables to personalize emails for each subscriber:
| Variable | Description |
| - | - |
| `{{FIRST_NAME}}` | Subscriber's first name |
| `{{LAST_NAME}}` | Subscriber's last name |
| `{{EMAIL}}` | Subscriber's email address |
| `{{UNSUBSCRIBE_URL}}` | Unsubscribe link |
To insert a variable, click **Variable** in Basics. Search the available fields or type your own name, such as `subscriber.plan` or `event.order.total`. You can paste a complete tag and add optional fallback text for missing values. Review the tag, then click **Insert variable**. It replaces selected text or inserts at your last text cursor; without a text cursor, it adds a paragraph after your selected block. Cancel leaves your content unchanged.
You can also type `{{` directly in a text block to use the inline picker.
## Preview & Testing
### Preview Mode
Toggle between desktop and mobile preview to see how your email looks on different devices.
### Send Test Email
Before sending a campaign:
1. Click **Send Test Email**
2. Pick up to 10 subscribers to preview as, or send to yourself
3. Check the email in your inbox
## Best Practices
### Design Tips
1. **Start with a logo** - Add brand recognition at the top
2. **Use clear headings** - Break up content with hierarchy
3. **One primary CTA** - Focus attention on the main action
4. **Add whitespace** - Use spacers to improve readability
5. **Include footer** - Always have unsubscribe and company info
### Content Tips
1. **Keep it concise** - Respect your readers' time
2. **Personalize** - Use variables for a personal touch
3. **Mobile-first** - Most emails are read on mobile
4. **Clear value** - Lead with what's in it for the reader
## Related
Send one-time email broadcasts
Build automated email workflows
Send triggered emails via API
Learn about subscriber data
# Migration Guide
Source: https://docs.sequenzy.com/guides/migration
How to migrate to Sequenzy without disrupting your existing email setup
Switching email platforms can feel risky. You don't want to mess up communications with your existing audience. This guide walks you through a safe, incremental migration that minimizes risk while letting you start benefiting from Sequenzy immediately.
## The Migration Philosophy
Don't do a big-bang migration. Instead:
1. **Start small** - Begin with transactional emails
2. **Prove the system** - Verify deliverability and tracking work
3. **Expand gradually** - Add sequences for new users
4. **Complete the switch** - Move your existing audience last
This approach means you're never "all-in" until you're confident everything works.
## Phase 1: Transactional Emails
**Risk: Minimal.** These are one-off emails you control completely.
Start by moving your transactional emails to Sequenzy. These are the easiest to migrate because:
* They're triggered by your code, so you control when the switch happens
* Each email is independent, no sequences to worry about
* If something goes wrong, you can quickly revert
### What to Move First
Pick 2-3 low-stakes transactional emails:
```
✓ Welcome email (new signups)
✓ Password reset
✓ Email verification
Later:
Order confirmation
Payment receipts
Shipping notifications
```
### Step-by-Step
**1. Create templates in Sequenzy**
Build your transactional email templates in the dashboard. Use the same content you currently have: don't redesign yet.
**2. Set up your sending domain**
Add and verify your domain in **Settings → Domain**. This takes a few hours for DNS propagation.
**3. Update your code**
Replace your current email sending with Sequenzy API calls:
```javascript theme={null}
// Before: Using your old provider
await oldProvider.send({
to: user.email,
template: "welcome",
data: { name: user.name },
});
// After: Using Sequenzy
await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: user.email,
slug: "welcome",
variables: { firstName: user.name },
}),
});
```
**4. Run both systems in parallel (optional)**
For extra safety, send from both systems for a week:
```javascript theme={null}
// Send from both during transition
await Promise.all([
oldProvider.send({ to: user.email, template: "welcome" }),
sequenzy.transactional.send({ to: user.email, slug: "welcome" }),
]);
```
Watch deliverability metrics on both. When you're confident, remove the old provider.
**5. Monitor and iterate**
Check Sequenzy analytics:
* Are emails being delivered?
* Are open rates comparable to your old system?
* Any bounce issues?
### How Long: 1-2 weeks
Get comfortable with the API, verify deliverability, build confidence.
***
## Phase 2: Sequences for New Users
**Risk: Low.** Only affects people who haven't received your old emails yet.
Now that transactional emails are working, set up automated sequences for new subscribers.
### Key Insight
New users have no expectation of your old email patterns. They won't notice or care that you switched platforms. This makes them perfect for testing your new sequences.
### What to Build
**Onboarding Sequence**
```
Trigger: New signup (tag: "new-signup")
│
▼
Day 0: Welcome email
│
▼
Day 2: Getting started tips
│
▼
Day 5: Feature highlight
│
▼
Day 7: Success story / case study
```
**Post-Purchase Sequence**
```
Trigger: Purchase event (saas.purchase)
│
▼
Day 0: Thank you + receipt
│
▼
Day 3: Getting the most from your purchase
│
▼
Day 7: Feedback request
```
**Trial Nurture Sequence**
```
Trigger: Trial started (saas.trial_started)
│
▼
Day 0: Trial welcome
│
▼
Day 3: Key feature walkthrough
│
▼
Day 7: Trial halfway check-in
│
▼
Day 12: Trial ending soon
│
▼
Day 14: Trial expired + offer
```
### Step-by-Step
**1. Map your triggers**
Identify when you want sequences to start:
```javascript theme={null}
// On user signup
await sequenzy.subscribers.create({
email: user.email,
firstName: user.firstName,
tags: ["new-signup"],
enrollInSequences: true,
});
// On purchase
await sequenzy.subscribers.events.trigger({
email: user.email,
event: "saas.purchase",
properties: { plan: subscription.plan },
});
```
**2. Build the sequences**
Use the visual editor to create your flows. Start simple: 3-5 emails max.
**3. Connect your app**
Add API calls at the right moments:
```javascript theme={null}
// During signup flow
async function handleSignup(user) {
// Create user in your database
await createUser(user);
// Add to Sequenzy
await fetch("https://api.sequenzy.com/api/v1/subscribers", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
tags: ["new-signup"],
customAttributes: {
userId: user.id,
signupSource: user.source,
},
}),
});
}
```
**4. Let it run**
Watch the sequences in action. Check:
* Are emails sending at the right times?
* Are conditions working correctly?
* What are open/click rates?
### How Long: 2-4 weeks
Build sequences, let them run with real users, iterate based on data.
***
## Phase 3: Migrate Your List
**Risk: Medium.** Now you're touching existing subscribers who may notice changes.
Once sequences are working for new users, import your existing subscriber list.
### Before You Import
**Clean your list first**
Don't import garbage:
```
Remove:
✗ Hard bounces
✗ Spam complaints
✗ Inactive 2+ years
✗ Invalid email formats
```
**Preserve subscription preferences**
Export status from your old system:
* Subscribed vs unsubscribed
* Subscription dates
* Tags/segments
Import unsubscribed contacts with `status: "unsubscribed"` rather than leaving
them out. That history is what stops a later import or form signup from
emailing someone who already opted out.
### Import Strategy
**1. Export from old system**
Get a CSV with:
```
email,firstName,lastName,status,createdAt,tags
john@example.com,John,Doe,active,2023-01-15,"customer,newsletter"
jane@example.com,Jane,Smith,unsubscribed,2022-06-20,"churned"
```
**2. Import to Sequenzy**
Use the dashboard CSV import (**Subscribers → Add contacts → Import CSV**) or
the [bulk import API](/api-reference/subscribers/import-create), which takes up
to 5,000 contacts per request:
```javascript theme={null}
// Bulk import via API, 5,000 contacts per request
for (let i = 0; i < subscribers.length; i += 5000) {
const batch = subscribers.slice(i, i + 5000);
const res = await fetch(
"https://api.sequenzy.com/api/v1/subscribers/imports",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
duplicateStrategy: "merge",
// Only for contacts who already opted in with your old provider. With
// double opt-in on, any other mode emails every imported contact.
optInMode: "confirmed",
idempotencyKey: `migration-batch-${i / 5000}`,
subscribers: batch.map((subscriber) => ({
email: subscriber.email,
firstName: subscriber.firstName,
lastName: subscriber.lastName,
status: subscriber.status,
tags: subscriber.tags.split(","),
customAttributes: { legacyId: subscriber.id },
})),
}),
}
);
if (!res.ok) throw new Error(`Batch ${i / 5000} failed: ${await res.text()}`);
}
```
For recurring or very large syncs, see
[Data Pipeline Ingestion](/guides/data-pipeline-ingestion).
**3. Verify the import**
Check counts match:
* Total subscribers
* By status (active vs unsubscribed)
* By key tags (customers, trial, etc.)
### Preserving Your Reputation
Your sender reputation is tied to your domain, not your email provider. As long as you're using the same sending domain, your reputation carries over.
If you're setting up a new domain:
* Warm it up gradually
* Start with your most engaged subscribers
* Increase volume over 2-4 weeks
### How Long: 1 week
Import is usually quick, but budget time for verification.
***
## Phase 4: Move Marketing Campaigns
**Risk: Higher.** Bulk emails to your full list.
This is the final step. You've proven the system works with transactional emails, sequences, and your imported list.
### Strategy: Don't Recreate Everything
You don't need to move every old campaign. Focus on:
```
✓ Move: Ongoing campaigns (newsletter, product updates)
✓ Move: High-performing templates
✓ Archive: One-time campaigns that won't repeat
✓ Rebuild: Underperforming campaigns (chance to improve)
```
### Running Your First Campaign
**1. Start with your most engaged segment**
Don't blast your entire list. Send to subscribers who:
* Opened an email in the last 30 days
* Are tagged as "engaged" or "active"
* Have been customers for 6+ months
```
Recipients: Filtered
Tags: contains "engaged"
Added: more_than 30 days ago
```
**2. Use a familiar format**
Your first campaign should look like your previous emails. Don't redesign: subscribers should feel continuity.
**3. Monitor closely**
Watch metrics in real-time:
* Bounces (should be \< 2%)
* Complaints (should be \< 0.1%)
* Opens (should match historical rates)
### Sunsetting Your Old System
Once you've sent 2-3 successful campaigns:
1. **Stop sending from old system**: Sequenzy is now primary
2. **Keep old system in read-only**: For historical data access
3. **Update any integrations**: Webhooks, Zapier, etc.
4. **Cancel old subscription**: After 30-60 days of success
### How Long: 2-4 weeks
Build confidence with engaged segments before expanding.
***
## The Complete Timeline
| Week | Phase | Focus |
| - | - | - |
| 1-2 | Transactional | Set up domain, move 2-3 transactional emails |
| 3-4 | Sequences | Build onboarding + post-purchase sequences |
| 5 | Import | Migrate subscriber list |
| 6-7 | Campaigns | First campaigns to engaged segments |
| 8+ | Complete | Full migration, sunset old system |
**Total: 6-8 weeks** for a careful migration.
***
## Migration Checklist
### Before Starting
* [ ] Export full subscriber list from old provider
* [ ] Document your current email templates
* [ ] List all triggered/transactional emails
* [ ] Map your subscriber tags/segments
* [ ] Note any integrations (Stripe, Zapier, etc.)
### Domain Setup
* [ ] Add sending domain in Sequenzy
* [ ] Configure DNS records (SPF, DKIM, DMARC)
* [ ] Verify domain
* [ ] Create sender profiles
### Transactional Emails
* [ ] Create transactional templates
* [ ] Update API calls in your code
* [ ] Test with internal emails
* [ ] Monitor deliverability for 1 week
### Sequences
* [ ] Design sequence flows on paper first
* [ ] Build sequences in dashboard
* [ ] Connect triggers in your code
* [ ] Test with new signups
* [ ] Monitor for 2 weeks
### List Import
* [ ] Clean your list (remove bounces, spam complaints)
* [ ] Export with all fields and tags
* [ ] Import to Sequenzy
* [ ] Verify counts match
* [ ] Check tag assignments
### Campaigns
* [ ] Send first campaign to engaged segment
* [ ] Monitor metrics closely
* [ ] Gradually expand to full list
* [ ] Update any automation tools
### Cleanup
* [ ] Disable sending from old provider
* [ ] Update webhook URLs
* [ ] Document new system for team
* [ ] Cancel old provider subscription
***
## Common Migration Pitfalls
### 1. Moving Too Fast
**Problem:** Migrating everything at once, something breaks, scrambling to fix.
**Solution:** Follow the phased approach. Each phase should run smoothly before moving on.
### 2. Not Cleaning Your List
**Problem:** Importing old bounces and spam traps tanks your reputation.
**Solution:** Clean before importing. Remove anyone who hasn't engaged in 2+ years.
### 3. Changing Design During Migration
**Problem:** Subscribers notice dramatic changes, engagement drops, you can't tell if it's the new platform or new design.
**Solution:** Keep designs identical during migration. Redesign after you're settled.
### 4. Losing Subscriber Preferences
**Problem:** Subscribers who unsubscribed start getting emails again.
**Solution:** Export and respect subscription status. Never email someone who opted out.
### 5. Not Monitoring
**Problem:** Issues go unnoticed until they're serious.
**Solution:** Check metrics daily during migration. Set up alerts for bounces and complaints.
***
## Need Help?
Migration questions? Reach out:
* **Email:** [nic@sequenzy.com](mailto:nic@sequenzy.com)
* **X:** [@nikpolale](https://x.com/nikpolale)
We've helped many teams migrate and are happy to review your migration plan.
# Sender Health & Thresholds
Source: https://docs.sequenzy.com/guides/sender-health
Understand how Sequenzy monitors your sending reputation and when warnings or pauses are triggered
Sequenzy monitors your email bounce and complaint rates to protect your sender reputation. We use volume-aware thresholds that become stricter as you send more emails.
## Thresholds by Volume
We use tiered thresholds based on your **total email volume** (all-time, not time-windowed) to determine when to warn or pause sending.
| Total Emails Sent | Complaint Warn | Complaint Pause | Permanent Bounce Warn | Permanent Bounce Pause | Temporary Bounce Warn | Temporary Bounce Pause |
| - | - | - | - | - | - | - |
| \< 100 | 1 complaint | 2 complaints | 10 bounces | 20 bounces | 20 bounces | 40 bounces |
| 100-499 | 2 complaints | 3 complaints | 3% | 5% | 6% | 10% |
| 500-1,999 | 2 complaints | 3 complaints | 3% | 5% | 6% | 10% |
| 2,000-19,999 | 3 complaints | 5 complaints | 3% | 5% | 6% | 10% |
| 20,000+ | 0.015% | 0.03% | 3% | 5% | 6% | 10% |
From 20,000 emails, the complaint limits are rates rather than fixed counts, so
a handful of complaints cannot pause a high-volume sender with a healthy rate.
For example, five complaints across 170,309 sends is about 0.003%, well below
the 0.03% pause rate. Below 20,000 emails, the fixed counts still apply. At
20,000 emails, 0.03% is six complaints, so the pause limit never drops below the
2,000-19,999 count.
Reaching a complaint limit triggers an AI review of the complained email. If the
primary review returns no usable decision, it tries a fallback model. A trusted,
nonbanned owner whose verified current, recent and queued complaint rates are all
below 0.1% (or the lower pause rate for their volume) keeps their current sending state
when the review is unavailable. If the review recommends a pause for that account,
it is flagged for human review instead of pausing sending. Neither path resets
counters or resumes an existing pause. High rates, missing metrics and AI abuse
(ban) decisions retain protective enforcement.
Permanent bounces (invalid or non-existent addresses) and temporary bounces
(full mailboxes, throttling, greylisting) are tracked separately, with looser
thresholds for temporary failures because they can recover on their own.
At low volumes (under 100 emails), we use **fixed counts** instead of
percentages because a single bad event could skew your rate dramatically. For
example, 1 complaint out of 3 emails would be a 33% complaint rate - clearly
not representative of your actual sending quality.
## How It Works
### Warning Level
When your rates exceed the warning threshold:
* A yellow banner appears on your dashboard
* Email sent to your account with tips to improve
**No sending is blocked at this level.** This is an early alert to review your email practices.
### Pause Level
When your rates exceed the pause threshold:
* Sending is **stopped** for the affected company workspace
* A red banner appears on your dashboard
* Email sent explaining the pause and next steps
Other companies owned by the same account continue sending unless they have
their own sender-health pause. A serious abuse finding can instead cause an
owner-account ban, which blocks every company that owner controls.
A pause blocks **every** send for that workspace: campaigns, sequence steps,
transactional email, and test sends. See [Restoring paused
sending](#restoring-paused-sending) for how to get it back.
## Restoring paused sending
Sender-health enforcement uses **all-time totals** counted from a reset
watermark, not a rolling window. A paused rate does not decay and there is no
expiry to wait for. Waiting will not restore sending.
A pause caused by a high **permanent-bounce** rate can be cleared by you, once
the cause is fixed. Every other pause reason (complaints, temporary bounces,
phishing guard, or a manual pause) needs a support review.
### 1. Fix the cause
1. Fix the source of the invalid addresses - the import, signup form, or
integration that let them in.
2. Leave every permanent bounce suppressed. Removing suppressions re-sends to
known-bad addresses and pauses the workspace again.
3. Require real mailbox proof (double opt-in or a verification step) on any
intake that produced the bad addresses.
### 2. Check the restore path
When sending is paused, Sequenzy runs an automated sender-health review of the
account. Check where it landed:
```bash CLI theme={null}
sequenzy sending status
```
```bash API theme={null}
curl "https://api.sequenzy.com/api/v1/sending-status" \
-H "Authorization: Bearer $SEQUENZY_API_KEY"
```
```text MCP theme={null}
get_sending_status
```
This returns the pause reason, the measured rates against the thresholds that
apply at your volume, the review status, ordered remediation steps, and
`selfResume.canSelfResume`. Reading it needs only the `account:read` scope, so a
read-only key can diagnose a blocked account.
If `selfResume.canSelfResume` is `false`, `selfResume.unavailableReason` names
the gate:
| `unavailableReason` | What it means | What to do |
| - | - | - |
| `waiting_for_review` | The automated review is still running | Re-check in a few minutes |
| `blocked_by_ai` | The review did not clear the pause | Email support |
| `review_failed` | The review could not complete | Email support |
| `blocked_by_admin` | Automatic resume is disabled for this account | Email support |
| `unsupported_reason` | This pause reason always needs a human | Email support |
### 3. Resume
Once `selfResume.canSelfResume` is `true`, confirm the cleanup and resume:
```bash CLI theme={null}
sequenzy sending resume --confirm-list-sanitized
```
```bash API theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/sending-status/resume" \
-H "Authorization: Bearer $SEQUENZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"listSanitizationConfirmed": true}'
```
```text MCP theme={null}
resume_sending with listSanitizationConfirmed: true
```
In the dashboard, the same action is the **Resume sending** button in the paused
banner, behind the same confirmation checkbox.
The confirmation dialog names the affected workspace, requires you to confirm the list cleanup, and explains what the reset changes before sending resumes.
Resuming requires the `companies:manage` scope plus owner or admin access. The
confirmation is recorded on the account's audit trail, so only send it once the
fix is actually live. On success the bounce watermark moves to now, so your rate
is recalculated from later sends, and the service attempts to requeue paused
campaigns plus due sequence steps. If part of that handoff is unavailable, the
successful response includes support guidance rather than reporting the resume
itself as failed.
Resuming never removes suppressions, and automatic resume is available a limited
number of times before a support review is required.
## Why These Thresholds?
Email service providers like Gmail and Yahoo require complaint rates under 0.1% for good deliverability. AWS SES (which powers Sequenzy's sending infrastructure) enforces similar limits.
Our tiered approach ensures:
1. **New senders aren't unfairly penalized** for a single bad email at low volume
2. **Growing senders are held to industry standards** as their volume increases
3. **High-volume senders are judged by complaint rate**, so a few complaints across a large volume do not pause healthy sending
## Examples
| Scenario | Total Emails | Total Complaints | Rate | Action | Why |
| - | - | - | - | - | - |
| 3 emails, 1 complaint | 3 | 1 | 33% | Warning | \< 100 tier: hits 1 warn count |
| 50 emails, 2 complaints | 50 | 2 | 4% | **Paused** | \< 100 tier: hits 2 pause count |
| 200 emails, 2 complaints | 200 | 2 | 1% | Warning | 100-499 tier: hits 2 warn count |
| 200 emails, 3 complaints | 200 | 3 | 1.5% | **Paused** | 100-499 tier: hits 3 pause count |
| 3,000 emails, 3 complaints | 3,000 | 3 | 0.1% | Warning | 2,000-19,999 tier: hits 3 warn count |
| 3,000 emails, 5 complaints | 3,000 | 5 | 0.167% | **Paused** | 2,000-19,999 tier: hits 5 pause count |
| 20,000 emails, 3 complaints | 20,000 | 3 | 0.015% | Warning | 20,000+ tier: hits 0.015% warn rate |
| 170,000 emails, 5 complaints | 170,000 | 5 | 0.003% | OK | 20,000+ tier: below 0.015% warn rate |
| 20,000 emails, 6 complaints | 20,000 | 6 | 0.03% | **Paused** | 20,000+ tier: hits 0.03% pause rate |
## Improving Your Sender Health
If you're seeing warnings or have been paused:
1. **Clean your list** - Remove invalid addresses and unengaged subscribers
2. **Use double opt-in** - Confirm subscribers before adding them to your list
3. **Make unsubscribing easy** - A clear unsubscribe is better than a spam complaint
4. **Send relevant content** - Segment your list and personalize your emails
5. **Maintain consistent sending** - Sporadic sending can hurt engagement rates
## FAQ
### Why are my emails going to spam?
First, check whether you're sending from a verified custom domain that you own. If you're using Sequenzy's free shared `sequenzymail.com` domain, your emails share sending reputation with other users on that domain. That shared reputation can make inbox placement less predictable, especially with Gmail, Outlook, and Yahoo.
For better deliverability, add and verify your own sending domain in Sequenzy, then send from an address on that domain. See [Domain Verification](/guides/domain-verification) for setup instructions.
### Why was my account paused with only a few complaints?
At low volumes (under 100 emails), we use fixed counts to protect your sender reputation early. While 2 complaints might seem small, it indicates a significant portion of recipients are marking your emails as spam. This early intervention prevents larger reputation damage. Fixed complaint counts apply below 20,000 emails; from 20,000 emails, only your complaint rate can trigger a complaint pause.
### How do I get unpaused?
If the pause was caused by a high permanent-bounce rate, fix the source of the
invalid addresses and then resume it yourself - see [Restoring paused
sending](#restoring-paused-sending). For any other pause reason, email
[nic@sequenzy.com](mailto:nic@sequenzy.com) with your account email, what caused the issue, and the fix you
shipped.
### Do these thresholds reset over time?
No. These are **total** metrics counted from a reset watermark, not a rolling
window, so a paused rate does not expire and waiting does not restore sending.
`GET /api/v1/sending-status` reports this explicitly: `metricsWindow.expiresAt`
is always `null`.
The rate only moves in two ways: as more legitimate sends accumulate in the
denominator, or when the watermark is reset - which happens when you resume a
paused workspace, or when support resets it for you.
### Why did my test send fail even though the email is valid?
A pause blocks test sends too. `send_test_email` and `send_sequence_test_email`
return a `SENDING_PAUSED` error with the pause reason and the steps to clear it.
Test sends are also excluded from the sender-health denominator, so they cannot
be used to dilute a bounce rate.
### What counts as a complaint?
A complaint is recorded when a recipient marks your email as spam using the "Report Spam" or similar button in their email client. This feedback is sent to us through feedback loops with major email providers.
### What types of bounces affect my rate?
Permanent and undetermined bounces count toward your bounce rate. Transient (temporary) bounces, such as a full mailbox, are tracked separately against their own, looser thresholds, because they often resolve on retry. A high transient rate can still point to a list or content problem.
## Account Review
If your company's sending is paused repeatedly or we detect severe abuse, support may review the broader account and can apply a real account ban when product access should be blocked entirely.
Company sender-health metrics and pauses are otherwise isolated. Billing is
different: companies with the same owner share the owner's email plan and usage
pool. See [Workspaces, Billing, and Shared
Limits](/concepts/workspaces-and-billing) for the complete scope breakdown,
including shared domains and dedicated IP reputation.
### When support review is required
Most permanent-bounce pauses can be cleared without us - see [Restoring paused
sending](#restoring-paused-sending). Email [nic@sequenzy.com](mailto:nic@sequenzy.com) when
`selfResume.canSelfResume` stays `false`, and include:
1. Your account email address and company ID
2. What caused the bounce or complaint issues
3. What steps you've taken to clean your email list and fix the intake
We'll review your sending and help you resume once we're confident the issues have been addressed.
## Related
Monitor your email performance in real-time
Improve deliverability with proper DNS setup
# Using Subdomains for Email
Source: https://docs.sequenzy.com/guides/subdomain-sending
Protect your domain reputation and isolate email risks by sending from a dedicated subdomain
When setting up email sending, one of the most important decisions is whether to send from your root domain (e.g., `example.com`) or a subdomain (e.g., `mail.example.com`). We strongly recommend using a subdomain for all email sending.
## Why Use a Subdomain?
### 1. Reputation Isolation
Email reputation is tied to the domain you send from. If something goes wrong (high bounce rates, spam complaints, or a misconfigured campaign), only the subdomain's reputation is affected.
| Scenario | Root Domain Impact | Subdomain Impact |
| - | - | - |
| Spam complaints spike | Your entire domain flagged | Only subdomain flagged |
| Blocklist addition | All email affected | Only subdomain email affected |
| Recovery time | Weeks to months | Days to weeks |
With a subdomain, your root domain remains pristine for critical communications like password resets, invoices, and personal emails.
### 2. Separation of Email Streams
Different types of email have different engagement patterns. Marketing emails typically have lower open rates than transactional emails. Mixing them can drag down your overall reputation.
**Recommended subdomain structure:**
| Subdomain | Use Case | Example |
| - | - | - |
| `mail.example.com` | Marketing & newsletters | Campaigns, sequences |
| `notifications.example.com` | Transactional | Order confirmations, receipts |
| `updates.example.com` | Product updates | Feature announcements |
### 3. Easier Troubleshooting
When deliverability issues arise, having separate subdomains makes it easier to:
* Identify which email stream is causing problems
* Isolate and fix issues without affecting other email
* Test changes on one subdomain before rolling out broadly
### 4. Flexibility for Future Changes
Using a subdomain gives you options:
* **Switch providers** without affecting your root domain
* **A/B test** different sending configurations
* **Sunset** a subdomain if it becomes compromised
## Choosing a Subdomain Name
Pick a subdomain that's professional and recognizable. Common choices:
| Subdomain | Best For |
| - | - |
| `mail` | General purpose, widely recognized |
| `email` | Alternative to mail |
| `news` | Newsletters |
| `updates` | Product updates |
| `notifications` | Transactional alerts |
Avoid generic names like `bounce`, `noreply`, or `mailer`: these can look
suspicious to spam filters and recipients.
## When to Use Your Root Domain
There are limited cases where sending from your root domain makes sense:
1. **Personal email** - If you're sending individual emails as yourself
2. **Very low volume** - A few emails per month with high engagement
3. **Already established** - You have years of good reputation on the root domain
Even then, consider the risk: one bad campaign could take months to recover from.
## Setting Up a Subdomain in Sequenzy
When you add a domain in Sequenzy, you can choose to use a subdomain:
1. Go to **Settings → Domain**
2. Click **Add Domain**
3. Enter your subdomain name (e.g., `mail`)
4. Complete DNS verification
The full sending domain will be `subdomain.yourdomain.com`.
Sequenzy suggests common subdomain names like `emails`, `mail`, `send`,
`news`, and `hello` to help you get started quickly.
## Subdomain vs Root Domain Comparison
| Factor | Subdomain | Root Domain |
| - | - | - |
| **Reputation risk** | Isolated | Shared with all email |
| **Recovery from issues** | Faster | Much slower |
| **DNS management** | Separate records | Shared records |
| **Professional appearance** | Standard practice | Can look more "official" |
| **Flexibility** | High | Low |
## Best Practices
### 1. One Subdomain Per Email Type
Don't mix marketing and transactional email on the same subdomain. Their different engagement patterns will muddy your reputation signals.
### 2. Warm Up New Subdomains
New subdomains have no reputation. Start with low volume to engaged recipients, then gradually increase:
* **Week 1**: 50-100 emails/day to most engaged users
* **Week 2**: 200-500 emails/day
* **Week 3**: 1,000+ emails/day
* **Week 4+**: Full volume
### 3. Monitor Each Subdomain Separately
Track metrics per subdomain:
* Open rates
* Bounce rates
* Spam complaints
* Blocklist status
### 4. Keep DNS Records Organized
Document which DNS records belong to which subdomain. This prevents accidental deletions and makes troubleshooting easier.
## Common Questions
### Will emails from a subdomain look suspicious?
No. Sending from subdomains is standard practice: Gmail, Amazon, and most major companies do it. Recipients see your brand name regardless.
### Can I use multiple subdomains?
Yes! Many companies use different subdomains for different purposes. Just verify each one separately in Sequenzy.
### What if my root domain is already damaged?
A new subdomain starts with a neutral reputation. This is actually a good recovery strategy: create a new subdomain and warm it up properly while the root domain recovers.
### Do subdomains inherit root domain reputation?
Partially. Mailbox providers consider domain relationships, but subdomains build their own reputation over time. A compromised root domain can affect subdomain deliverability initially.
## Related
Set up DNS records for your subdomain
Protect your domain from spoofing
# Supabase Custom SMTP Setup Guide
Source: https://docs.sequenzy.com/guides/supabase-custom-smtp
Configure custom SMTP in Supabase Auth to send invites, magic links, password resets, and confirmation emails reliably from your own domain - in under 5 minutes.
Supabase ships with a built-in email sender that's great for testing - and not much else. It rate-limits to **2 emails per hour** on the free tier, sends from a generic `noreply@mail.app.supabase.io` address, and lands in spam often enough that real users never finish signing up.
Configuring **custom SMTP in Supabase** fixes all three problems. This guide walks through the setup using Sequenzy as your SMTP provider, but the same steps apply to any SMTP service.
## Why You Need Custom SMTP in Supabase
The default Supabase email sender exists so you can verify your auth flow works. The moment you have real users, the default sender becomes a liability:
* **Strict rate limits** - 2 emails per hour on free, 30/hour on Pro. A signup spike will start dropping confirmations.
* **Generic sender domain** - `noreply@mail.app.supabase.io` looks like a phishing attempt to most inbox providers and to your users.
* **Bad deliverability** - the shared Supabase IP pool is reused by thousands of projects. One bad actor's reputation hits everyone.
* **No template control beyond the basics** - Supabase's built-in editor handles substitution and that's it. No design system, no preview tooling, no versioning.
Switching to custom SMTP solves rate limits and sender identity. Pairing it with a dedicated transactional provider like [Sequenzy](https://www.sequenzy.com) also solves deliverability and template management.
## Before You Start
You'll need:
1. A Supabase project with an Auth instance
2. An SMTP provider account (this guide uses Sequenzy)
3. A verified sending domain (your provider will walk you through SPF/DKIM/DMARC setup)
If you don't have an SMTP provider yet, [Sequenzy gives you a shared-domain sender automatically](https://www.sequenzy.com) so you can ship without configuring DNS first - then move to your own domain when you're ready.
## Step 1: Get Your SMTP Credentials
In Sequenzy:
1. Open **Settings → Integrations** and click **Connect** next to Supabase
2. Switch to the **Auth emails** tab
3. Click **Generate key** to create an API key for the Supabase SMTP user
You'll see four values:
| Setting | Value |
| - | - |
| Host | `smtp.sequenzy.com` |
| Port | `587` |
| Username | `api` |
| Password | Your Sequenzy API key |
Copy the API key now - it's only shown once.
## Step 2: Enable Custom SMTP in Supabase
1. Open your project at [supabase.com/dashboard](https://supabase.com/dashboard)
2. In the left sidebar, go to **Authentication**
3. Under **Notifications**, click **Emails**
4. Scroll to **SMTP Settings** and toggle **Enable Custom SMTP**
The exact path is **Authentication → Emails (under Notifications) → SMTP
Settings**. Earlier Supabase versions called this section "SMTP Configuration"
and placed it under Project Settings - it's now lives inside the Auth section.
## Step 3: Paste the Credentials
Fill in the SMTP form with the values from Step 1:
* **Host**: `smtp.sequenzy.com`
* **Port**: `587`
* **Minimum interval**: `60` (seconds between emails - keep low for auth flows)
* **Username**: `api`
* **Password**: paste your Sequenzy API key
* **Sender email**: an address on your verified sending domain
* **Sender name**: your product name (shown in users' inboxes)
Click **Save**.
## Step 4: Send a Test Email
In the Supabase dashboard, trigger any auth email - the easiest test is **Authentication → Users → Add user → Send invite**. Use a real inbox you control.
If the invite arrives within a few seconds, you're done. If it doesn't:
* Check **Sequenzy → Activity** to see if the email was received and delivered
* Verify the sender email matches a verified Sequenzy domain
* Confirm the API key was pasted without surrounding whitespace
## Step 5 (Recommended): Use Sequenzy Templates Instead of Supabase's
Supabase's built-in template editor is fine for plain emails. For branded auth flows - product onboarding, magic links that match your design system, multi-language support - point Supabase's templates at Sequenzy transactional emails instead.
Sequenzy ships pre-built default templates for every Supabase auth flow. In the **Connect Supabase** modal, click **Create default templates** and you'll get drafts for:
* Confirm signup
* Invite user
* Magic link
* Password reset
* Email change
* Password changed
* Phone changed
* Identity linked / unlinked
* MFA enrolled / removed
Then in Supabase, replace each template body with a JSON payload referencing the matching Sequenzy slug. See the [full Supabase integration docs](/integrations/supabase) for the JSON snippets and variable mapping.
## Common Issues
### "Error sending magic link email" / "Error sending confirmation email"
Almost always one of:
1. SMTP credentials are wrong - re-paste the password
2. The **Sender email** doesn't belong to a verified domain on your SMTP provider
3. Your provider rejected the mail - check the provider's activity log for the real error
### Emails go to spam
* Set up SPF, DKIM, and DMARC for your sending domain
* Don't send from `noreply@` - use a real reply-to address
* Warm up new domains gradually before sending high volume
See the [Sequenzy domain verification guide](/guides/domain-verification) and [DMARC guide](/guides/dmarc) for step-by-step DNS setup.
### Rate limits still hitting after switching
Supabase has its own per-hour rate limit on auth emails, separate from your SMTP provider. Bump it under **Authentication → Rate Limits → Email sent**.
## FAQ
### Does Supabase support custom SMTP on the free tier?
Yes. Custom SMTP is available on every Supabase plan, including free.
### What port should I use for Supabase SMTP?
Use **587** with STARTTLS. Port 465 (SMTPS) also works with most providers but is less universally supported.
### Can I use Gmail or my personal email account as SMTP?
Technically yes, practically no. Gmail's SMTP is rate-limited to \~500 emails per day, blocks anything that looks like marketing or bulk auth flows, and breaks the moment you enable 2FA. Use a transactional email provider designed for app email - [like Sequenzy](https://www.sequenzy.com).
### Why are my Supabase auth emails slow?
Supabase calls SMTP synchronously inside the auth request. Slow SMTP = slow signups. Pick a provider with sub-second send times - Sequenzy averages under 300ms from API to inbox handoff.
### Does this work for password reset emails too?
Yes - once custom SMTP is enabled, **all** Supabase auth emails (signup confirmation, magic links, invites, password reset, email change, MFA, security notifications) route through your SMTP provider.
## Next Steps
* [Connect Supabase to Sequenzy](/integrations/supabase) for full auth email + user sync
* [Verify your sending domain](/guides/domain-verification) for the best deliverability
* [Set up DMARC](/guides/dmarc) to prevent spoofing of your auth emails
# Affonso Integration
Source: https://docs.sequenzy.com/integrations/affonso
Sync affiliates and trigger email automations on referrals, commissions, and payouts
Connect your Affonso affiliate program to sync affiliates into Sequenzy as subscribers and trigger email automations across the whole affiliate lifecycle: signups, referrals, commissions, coupons, and payouts.
## What is Affonso?
[Affonso](https://affonso.io) is an affiliate marketing platform for SaaS companies. It tracks referral links, conversions, commissions, and payouts for your affiliate program.
## Connecting Affonso
### Step 1: Get Your Webhook URL
1. Go to **Settings → Integrations** in Sequenzy
2. Find **Affonso** under Affiliate Marketing
3. Click **Connect**
4. Copy the **Webhook URL** shown
### Step 2: Create an Affonso API Key
1. Open [Connect → API](https://affonso.io/app/affiliate-program/connect/api) in Affonso
2. Click **Create Key**
3. Grant the key **Affiliates: read**, **Referrals: read**, and
**Commissions: read** permissions
4. Copy the API key (it is shown only once)
Sequenzy stores the key encrypted and uses it to backfill affiliates that
existed before you connected the integration, including their current
performance totals.
### Step 3: Create a Webhook Endpoint in Affonso
1. Go to your Affonso dashboard
2. Navigate to **Settings → Webhooks**
3. Click **Add Endpoint** and paste your Sequenzy webhook URL
4. Select **All events** (recommended), or pick the affiliate, referral, transaction, coupon, and payout events you need
5. Copy the **Signing Secret** (it is shown only once)
### Step 4: Complete the Connection in Sequenzy
1. Return to Sequenzy's Affonso connection modal
2. Paste the **API Key** and **Signing Secret**
3. Click **Connect Affonso**
That's it! Sequenzy queues an initial import of your existing affiliates, then
keeps them current from Affonso events. Every webhook is verified against the
signing secret before it is processed.
## What Gets Synced
During the initial backfill and whenever an affiliate signs up or is updated in
Affonso, Sequenzy creates or updates a subscriber with the affiliate's email,
name, and these attributes. Attribute names are provider-agnostic (`affiliate*`,
not `affonso*`), so your segments and automations keep working if you ever
switch affiliate platforms:
| Attribute | Source |
| - | - |
| `affiliateId` | Affonso affiliate id (used to match later events) |
| `affiliateTrackingId` | The affiliate's referral/tracking code |
| `affiliateStatus` | Partnership status, such as `PENDING` or `APPROVED` |
| `affiliateProgramId` | The affiliate program id |
| `affiliateGroupId` | Group association (when set) |
| `affiliateCouponCode` | The affiliate's coupon code (from coupon events) |
| `affiliateCouponProvider` | Payment provider of the coupon (stripe, paddle, etc.) |
| Custom metadata | Primitive values from the affiliate's metadata |
The initial sync imports Affonso's absolute performance totals. Relevant
referral, transaction, and payout webhooks update the subscriber immediately,
then queue an absolute refresh so retries, refunds, deletions, and missed events
cannot leave the totals drifting:
| Attribute | Meaning |
| - | - |
| `affiliateLeadCount` | Signups the affiliate has referred |
| `affiliateConversionCount` | Successful attributed sales |
| `affiliateTotalReferredRevenue` | Lifetime sale value the affiliate referred |
| `affiliateTotalCommissionEarned` | Lifetime commission the affiliate earned |
| `affiliateTotalPaidOut` | Total commission actually paid out |
| `affiliateLastPayoutAt` | Timestamp of the most recent payout |
Failed and refunded sales are not counted, and the universal `transaction.updated` event never double counts totals.
Referral, transaction, coupon, and payout events identify the affiliate by
their Affonso affiliate id. The affiliate must already be synced (via an
`affiliate.created`, `affiliate.confirmed`, or `affiliate.updated` webhook)
before these events can be matched. The initial API backfill creates those
subscriber profiles for affiliates who predate the connection; unmatched
events are acknowledged and skipped.
The initial backfill sets current profile and performance attributes. It does
not replay old `affiliate.*` events or enroll subscribers into sequences.
## Event Naming
Events live in the provider-agnostic `affiliate.*` namespace and target the affiliate's subscriber profile. Affiliate lifecycle events keep Affonso's names; referral, transaction, coupon, and payout events are folded into the same namespace:
| Affonso Event | Sequenzy Event |
| - | - |
| `affiliate.created` | `affiliate.created` |
| `affiliate.confirmed` | `affiliate.confirmed` |
| `affiliate.updated` | `affiliate.updated` |
| `affiliate.deleted` | `affiliate.deleted` |
| `referral.lead` | `affiliate.referral_lead` |
| `referral.converted` | `affiliate.referral_converted` |
| `referral.updated` | `affiliate.referral_updated` |
| `referral.deleted` | `affiliate.referral_deleted` |
| `transaction.created` | `affiliate.transaction_created` |
| `transaction.approved` | `affiliate.transaction_approved` |
| `transaction.paid` | `affiliate.transaction_paid` |
| `transaction.rejected` | `affiliate.transaction_rejected` |
| `coupon.created` | `affiliate.coupon_created` |
| `coupon.deleted` | `affiliate.coupon_deleted` |
| `payout.created` | `affiliate.payout_created` |
| `payout.paid` | `affiliate.payout_paid` |
| `payout.failed` | `affiliate.payout_failed` |
## Using with Automations
### Affiliate Welcome Sequence
1. Go to **Automations** → **Create New**
2. Set trigger to **Event Received** → `affiliate.created`
3. Send a welcome email with their tracking link: `Your referral code is {{affiliateTrackingId}}`
### Celebrate Conversions
Send affiliates a congratulations email whenever they refer a paying customer:
```text theme={null}
Trigger: affiliate.referral_converted
└── Send "You just earned a commission!" email
with {{event.referredEmail}} and {{event.trackingId}}
```
### Payout Notifications
Trigger on `affiliate.payout_paid` to confirm payouts, using `{{event.amount}}` and `{{event.invoiceNumber}}` in the email body.
### Reward Top Affiliates
Create a segment where `affiliateTotalReferredRevenue` is greater than 1000 and send them an upgraded commission offer or exclusive coupon.
## Event Properties as Variables
Each event exposes its details as `{{event.*}}` variables in automation emails:
* Referrals: `referralId`, `trackingId`, `referralStatus`, `referredEmail`, `customerId`, `referrer`
* Transactions: `transactionId`, `saleAmount`, `saleCurrency`, `saleStatus`, `commissionAmount`, `commissionStatus`, `incentiveType`
* Payouts: `payoutId`, `amount`, `status`, `invoiceNumber`, `paidAt`
* Coupons: `couponId`, `code`, `paymentProvider`
## Troubleshooting
### Events Not Appearing
1. Verify the webhook URL in Affonso matches the URL shown in Sequenzy
2. Check that the signing secret you pasted matches the endpoint's secret in Affonso
3. Use the **Activity** button on the Affonso integration row to inspect recent webhook deliveries and errors
### Referral or Payout Events Being Skipped
These events are matched through the `affiliateId` attribute. Check the Affonso
integration row to confirm the initial affiliate backfill completed. If it
failed, reconnect with an active API key that has **Affiliates: read**,
**Referrals: read**, and **Commissions: read** permissions.
### Signature Verification Failing
Affonso's live service signs the exact raw request body with HMAC-SHA256 and
sends it as `sha256=<64-character hex digest>` in `X-Affonso-Signature`.
Affonso may instead send the Unix time in `X-Affonso-Timestamp` and a bare
digest of `.` in `X-Affonso-Signature`; Sequenzy accepts
that format too, along with the older combined `t=...,v1=...` form. If your
endpoint secret was rotated in Affonso, reconnect the integration in Sequenzy
with the new signing secret.
## FAQ
Yes. Affiliates are synced as regular subscribers so you can email them,
segment them, and enroll them in sequences.
No. Referral events carry the referred customer's email as an event property
(`{{event.referredEmail}}`), but only the affiliate is created or updated as
a subscriber. Referred customers usually enter Sequenzy through your normal
signup or payment integrations.
No. The backfill creates or updates subscriber profiles without replaying
historical events or enrolling those subscribers into sequences. New Affonso
webhook events behave normally after the connection is active.
Their `affiliateStatus` attribute is set to `DELETED` and an
`affiliate.deleted` event fires. The subscriber stays active so you keep
history; build an automation on that event if you want to tag or unsubscribe
them.
No. `affiliateTotalReferredRevenue` and `affiliateTotalCommissionEarned` are
lifetime gross totals. Refunds emit `affiliate.transaction_rejected` events
you can automate on, but totals are not decremented.
Yes, the Affonso integration is included at no extra cost.
# Attio Integration
Source: https://docs.sequenzy.com/integrations/attio
Upsert people into Attio when they join a Sequenzy list
Push new list subscribers into [Attio](https://attio.com) as people. Map a Sequenzy list to an Attio list, and each join upserts the person by email, optionally matches a company from the email domain, and adds them to that Attio list.
This is outbound only. Attio does not send events back to Sequenzy, and there is no inbound webhook to configure.
## How it works
1. Connect with an Attio access token from Workspace settings → Developers
2. Map each Sequenzy list you care about to an Attio list
3. When someone joins a mapped list (`subscriber.list_subscribed`), Sequenzy upserts them as a person in Attio and adds them to the mapped list
4. If company matching is on, a non-free-mail domain (not Gmail, Outlook, and similar) is upserted as a company and linked on the person
Unmapped lists are ignored. Leaving a Sequenzy list does not remove the person from Attio: the sync is add-only.
## Connecting Attio
### Dashboard
1. Go to **Settings → Integrations** in Sequenzy
2. Find **Attio** under CRM
3. Click **Connect**
4. In Attio, open **Workspace settings → Developers**, create an access token, and grant `object_configuration:read`, `record_permission:read-write`, `list_configuration:read`, and `list_entry:read-write`
5. Paste the token into Sequenzy and connect
6. Map your Sequenzy lists to Attio lists, then save
Name matches are suggested when nothing is saved yet (for example a Sequenzy list named "Field Guide" maps to an Attio list with the same name).
### API, CLI, and MCP
Attio connects with an access token. There is no webhook URL to configure.
* **API** - `POST /api/v1/integrations/connect` with `{ "provider": "attio", "apiKey": "attio_..." }`. Optional `settings.listMap` (Sequenzy list id → Attio list UUID or api slug) and `settings.syncCompanyFromDomain` (default `true`). [Connect docs](/api-reference/integrations/connect)
* **CLI** - `sequenzy integrations connect attio --api-key attio_... --attio-list list_123=field-guide`. Reconnect with `--attio-clear-lists` to remove every mapping, or use `--attio-enable-company-domain` / `--attio-disable-company-domain` to change company matching.
* **MCP** - On the standard MCP surface, call `connect_integration` with `provider: "attio"` and `apiKey: "attio_..."`. Credential-based connection is not available in the ChatGPT app; use the dashboard or CLI there.
Reconnect with the same call to replace the stored token. Mapping after connect does **not** need the token again:
```bash theme={null}
sequenzy integrations attio int_123
sequenzy integrations set-attio-lists int_123 --map list_abc=field-guide list_def=benchmark
```
Same actions as [`GET /api/v1/integrations/{id}/attio`](/api-reference/integrations/attio-get), [`PATCH /api/v1/integrations/{id}/attio`](/api-reference/integrations/attio-update), and the MCP tools `get_attio_mapping` / `update_attio_settings`. The dashboard **Map lists** flow still works.
## What gets synced
| Sequenzy | Attio |
| - | - |
| Subscriber email | Person `email_addresses` (match key) |
| First and last name | Person `name`, when present |
| Email domain (work addresses only) | Company `domains` (match key), then linked on the person |
| Mapped list membership | List entry on the Attio list |
Gmail, Outlook, and other free-mail domains are never used for company matching.
## Things to know
* **Add-only.** Unsubscribing, leaving a list, or deleting a subscriber in Sequenzy does not remove the person from Attio.
* **No backfill.** People already on a Sequenzy list are not pushed until they join (or rejoin) after the list is mapped.
* **No inbound webhook.** Point nothing at Sequenzy; the access token is enough.
* **Live only.** Sync runs when someone is added to a mapped list, not on a schedule.
* **Activity.** Each upsert is logged on the integration's Activity view.
## Scopes
The Attio token needs `object_configuration:read`, `record_permission:read-write`, `list_configuration:read`, and `list_entry:read-write` so Sequenzy can identify the workspace, list compatible people lists, upsert people and companies, and add list entries. Sequenzy rejects active tokens that are missing any of these permissions.
# Better Auth Integration
Source: https://docs.sequenzy.com/integrations/better-auth
Automatically sync users from Better Auth to create subscribers
Sync your [Better Auth](https://www.better-auth.com) users to Sequenzy with a simple plugin. When users sign up, they're automatically added as subscribers, enabling welcome emails, onboarding sequences, and more.
## Installation
Install the official Sequenzy plugin for Better Auth:
```bash theme={null}
npm install @sequenzy/better-auth
# or
bun add @sequenzy/better-auth
# or
pnpm add @sequenzy/better-auth
```
## Quick Setup
### 1. Get Your API Key
1. Go to **Settings → API Keys & SMTP** in Sequenzy
2. Click **Create API Key**
3. Copy the key and add it to your environment variables:
```bash theme={null}
SEQUENZY_API_KEY=seq_live_xxxxxxxxxxxxx
```
### 2. Add the Plugin
Add the Sequenzy plugin to your Better Auth configuration:
```ts auth.ts theme={null}
import { betterAuth } from "better-auth";
import { sequenzy } from "@sequenzy/better-auth";
export const auth = betterAuth({
// ... your existing config
plugins: [
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
}),
],
});
```
### 3. Add Client Plugin (Optional)
For TypeScript type inference, add the client plugin:
```ts auth-client.ts theme={null}
import { createAuthClient } from "better-auth/client";
import { sequenzyClient } from "@sequenzy/better-auth/client";
export const authClient = createAuthClient({
plugins: [sequenzyClient()],
});
```
That's it! New users will now be automatically synced to Sequenzy.
## Configuration Options
```ts theme={null}
sequenzy({
// Required: Your Sequenzy API key
apiKey: process.env.SEQUENZY_API_KEY!,
// Optional: Add subscribers to specific lists only
// Default: Uses your workspace default lists setting
listIds: ["list_welcome", "list_newsletter"],
// Optional: Tags to assign to new subscribers
tags: ["signup", "better-auth"],
// Optional: Enroll in automation sequences
// Default: true
enrollInSequences: true,
// Optional: Extract custom attributes from the user
getCustomAttributes: (user) => ({
authProvider: "better-auth",
signupDate: new Date().toISOString(),
}),
// Optional: Success callback
onSubscriberCreated: ({ userId, email, subscriberId }) => {
console.log(`Synced ${email} to Sequenzy`);
},
// Optional: Error callback
onError: (error, user) => {
console.error(`Failed to sync ${user.email}:`, error.message);
},
});
```
## What Gets Synced
When a user signs up through Better Auth, Sequenzy automatically:
1. Creates a new subscriber with their email
2. Syncs profile data (name parsed into first/last)
3. Adds to specified lists (or your workspace default lists if not specified)
4. Triggers any matching automations
### Synced Attributes
| Attribute | Source |
| - | - |
| `email` | User's email address |
| `firstName` | First word of user's name |
| `lastName` | Remaining words of user's name |
| Custom attributes | Via `getCustomAttributes` option |
## List Assignment
### Use Workspace Default Lists
By default, new subscribers use the default lists you choose in workspace
settings:
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
// No listIds = workspace default lists
});
```
### Add to Specific Lists
To add subscribers to specific lists only:
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
listIds: ["list_abc123", "list_def456"],
});
```
### Add to No Lists
To create subscribers without list assignment:
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
listIds: [], // Empty array = no lists
});
```
## Supported Auth Methods
The plugin hooks into these Better Auth flows:
| Method | Endpoint | Synced |
| - | - | - |
| Email/Password | `/sign-up/email` | Yes |
| Google OAuth | `/callback/google` | Yes |
| GitHub OAuth | `/callback/github` | Yes |
| Other OAuth | `/callback/*` | Yes |
Both new registrations and new OAuth sign-ups are synced. Returning OAuth users (already have an account) are not re-synced.
## Using with Automations
### Welcome Sequence
Create an automation to welcome every new user:
1. Go to **Automations** → **Create New**
2. Set trigger to **Contact Added**
3. Add your welcome email sequence
4. Activate the automation
Every Better Auth signup will now receive your welcome sequence.
### Personalization
Use synced attributes in your emails:
```
Hi {{firstName}},
Welcome! We're excited to have you on board.
```
## Error Handling
The plugin is non-blocking by design. If Sequenzy is unavailable:
* Authentication succeeds normally
* User account is created
* Error is logged (if `onError` is set)
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
onError: (error, user) => {
// Log to your error tracking service
Sentry.captureException(error, {
extra: { userEmail: user.email },
});
},
});
```
## Custom Attributes
Add any data you want to track:
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
getCustomAttributes: (user) => ({
userId: user.id,
hasAvatar: !!user.image,
signupSource: "web",
signupTimestamp: Date.now(),
}),
});
```
These attributes are available in:
* Subscriber profiles
* Email personalization
* Segment filters
* Automation conditions
## Tagging Signups
Tag new subscribers for organization or automation triggers:
```ts theme={null}
sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
tags: ["signup", "better-auth", "web-app"],
});
```
You can then:
* Filter subscribers by tag
* Trigger automations on tag assignment
* Create segments based on tags
## FAQ
No, only new sign-ups after adding the plugin are synced. Existing users need to be imported separately via the API or CSV upload.
The plugin uses Sequenzy's upsert API: existing subscribers get their
attributes merged, not duplicated.
Yes, set `enrollInSequences: false` to create subscribers without triggering
automations.
No. The plugin runs asynchronously after authentication succeeds. Auth flow is
never delayed or blocked.
Authentication succeeds normally. The sync failure is logged via `onError` if
provided.
Yes, the Better Auth integration is included at no extra cost.
## Source Code
The plugin is open source:
View source, report issues, or contribute
# Chargebee Integration
Source: https://docs.sequenzy.com/integrations/chargebee
Sync revenue data from Chargebee to power segments and automations
Connect your Chargebee site to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV, plus automations triggered by every billing event.
## What is Chargebee?
[Chargebee](https://www.chargebee.com) is a subscription billing and revenue management platform. It handles plans, trials, invoicing, dunning, and payments across multiple gateways, and is widely used by SaaS and subscription businesses.
## Connecting Chargebee
Chargebee requires an API key and webhook setup. Here's how to connect:
### Step 1: Get Your Chargebee API Key
1. Go to your Chargebee dashboard (`https://your-site.chargebee.com`)
2. Navigate to **Settings → Configure Chargebee → API Keys**
3. Create a key (a **read-only key** is sufficient) and copy it
### Step 2: Find Your Site Name
Your site name is the subdomain of your Chargebee dashboard URL. If your dashboard lives at `acme.chargebee.com`, your site name is `acme` (test sites usually end in `-test`, e.g. `acme-test`).
### Step 3: Set Up Webhooks in Chargebee
1. In Chargebee, go to **Settings → Configure Chargebee → Webhooks**
2. Click **Add Webhook**
3. Enter your Sequenzy webhook URL:
```
https://api.sequenzy.com/api/webhooks/chargebee/{your-company-id}
```
You can find this URL in Sequenzy when connecting Chargebee - it's
automatically shown in the connection modal.
4. Enable **basic authentication** on the webhook and choose a username and password
5. Chargebee sends all events by default, so no per-event selection is needed. The events Sequenzy acts on include:
* `payment_succeeded`
* `payment_failed`
* `payment_refunded`
* `subscription_created`
* `subscription_activated`
* `subscription_cancellation_scheduled`
* `subscription_cancelled`
* `subscription_paused`
* `subscription_resumed`
* `subscription_reactivated`
* `subscription_deleted`
* `subscription_trial_end_reminder`
6. Save the webhook
### Step 4: Connect in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Chargebee
3. Enter your:
* **Site Name** (from Step 2)
* **API Key** (from Step 1)
* **Webhook Username and Password** (the same credentials from Step 3)
4. Click **Connect Chargebee**
Sequenzy will immediately run an initial sync of your Chargebee customers, subscriptions, and paid invoices, and begin receiving webhook events.
Webhook requests are authenticated with the basic auth credentials you
entered. Requests without matching credentials are rejected, so make sure the
username and password in Chargebee exactly match the ones entered in Sequenzy.
## Initial Sync
When you connect (or click **Sync Revenue** later), Sequenzy imports your full Chargebee history:
* **Customers** become subscribers, matched or created by email
* **Subscriptions** determine status tags and MRR
* **Paid invoices** are summed into each customer's LTV
The sync is idempotent - running it again updates existing subscribers instead of creating duplicates.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
| `chargebeeCustomerId` | String | The customer's ID in Chargebee |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is (divided by the billing period for multi-month plans)
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` and `in_trial` subscriptions count toward MRR. Both Product Catalog 1.0 (`plan_amount`) and 2.0 (`subscription_items`) sites are supported, and non-USD currencies are converted automatically.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Chargebee subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active subscription |
| `trial` | Has a subscription in trial |
| `cancelled` | Subscription is scheduled to cancel |
| `churned` | Subscription has been cancelled or paused |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes via webhooks.
## Tracked Events
When you connect Chargebee, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | Payment received (new subscription, renewal, or upgrade) |
| `saas.purchase.monthly` | Payment for a monthly subscription |
| `saas.purchase.yearly` | Payment for a yearly subscription |
| `saas.payment_failed` | Payment attempt failed |
| `saas.trial_started` | Subscription created in trial |
| `saas.trial_will_end` | Chargebee's trial-ending reminder fired |
| `saas.trial_ended` | Trial converted to an active subscription |
| `saas.trial_cancelled` | Subscription cancelled before ever activating |
| `saas.cancelled` | Cancellation scheduled for the end of the billing term |
| `saas.churn` | Subscription cancelled, deleted, or paused |
| `saas.refund` | Payment refunded |
## Idempotency and Retries
Every Chargebee event has a unique ID. Sequenzy records processed event IDs per subscriber, so duplicate webhook deliveries and Chargebee's automatic retries never double-count MRR or LTV. Failed event processing returns a success acknowledgment to avoid retry storms, and the periodic **Sync Revenue** action reconciles any missed events from the API.
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100` - High MRR customers
* `ltv > 1000` - Lifetime high-value customers
* `mrr >= 50 AND mrr < 200` - Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500` - High-value churned customers (win-back targets)
* `tag = cancelled AND mrr >= 100` - At-risk high-value customers needing attention
* `tag = trial` - Everyone currently trialing, for conversion campaigns
### Automations
Trigger automations from Chargebee billing events:
* Start an onboarding sequence on `saas.trial_started`
* Send conversion nudges on `saas.trial_will_end`
* Trigger dunning emails on `saas.payment_failed`
* Launch a win-back sequence when a subscriber gets the `churned` tag
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Chargebee customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If a Chargebee customer doesn't have an email in the webhook or doesn't match any subscriber, the event is acknowledged but no action is taken.
## Troubleshooting
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Chargebee settings
2. Check Chargebee's webhook logs (**Settings → Configure Chargebee → Webhooks**) for failed delivery attempts
3. Ensure basic authentication is enabled on the webhook
### Webhook Authentication Failing
If webhooks are rejected with authentication errors:
1. Verify the username and password in Chargebee match what you entered in Sequenzy exactly
2. Reconnect the integration in Sequenzy to update the stored credentials
### Sync Fails or Shows an Error
1. Verify the API key is valid and hasn't been revoked in Chargebee
2. Confirm the site name matches your Chargebee subdomain (including `-test` for test sites)
3. Check the integration's status in **Settings → Integrations** for the specific error message
# Clerk Integration
Source: https://docs.sequenzy.com/integrations/clerk
Automatically sync users from Clerk to create subscribers
Connect your Clerk authentication to automatically add every new user as a subscriber. No code required: just paste a webhook URL.
## What is Clerk?
[Clerk](https://clerk.com) is a modern authentication platform that provides drop-in user management for web applications. It handles sign-up, sign-in, user profiles, and security.
## Connecting Clerk
### Step 1: Get Your Webhook URL
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Clerk
3. Copy the webhook URL shown (format: `https://api.sequenzy.com/api/webhooks/clerk/{your-company-id}`)
### Step 2: Create Webhook in Clerk
1. Go to your [Clerk Dashboard](https://dashboard.clerk.com)
2. Navigate to **Developers** in the left sidebar. Click on **Webhooks**
3. Click **Add Endpoint**
4. Paste your Sequenzy webhook URL
5. Select these events:
* `user.created`
* `user.updated`
* `user.deleted`
6. Click **Create**
7. Copy the **Signing Secret** (format: `whsec_xxxxxxxxx`)
The signing secret is only shown once. Copy it before closing the dialog.
### Step 3: Complete Connection in Sequenzy
1. Return to Sequenzy's Clerk connection modal
2. Paste the **Signing Secret** from Clerk
3. Click **Connect Clerk**
That's it! New Clerk users will automatically become subscribers.
## What Gets Synced
When a user signs up through Clerk, Sequenzy automatically:
1. Creates a new subscriber with their email
2. Syncs first and last name
3. Triggers the `contact.subscribed` internal event
### Synced Attributes
| Attribute | Source |
| - | - |
| `email` | Primary verified email address |
| `firstName` | Clerk first name |
| `lastName` | Clerk last name |
| `clerkUserId` | Clerk user ID |
## Tracked Events
| Event | Triggered When |
| - | - |
| `contact.subscribed` | New user created in Clerk |
| `contact.unsubscribed` | User deleted in Clerk |
## Using with Automations
### Welcome Sequence
Create an automation triggered by `contact.subscribed` to send a welcome sequence to every new user:
1. Go to **Automations** → **Create New**
2. Set trigger to **Event Received** → `contact.subscribed`
3. Add your welcome email sequence
4. Activate the automation
Every Clerk signup will now receive your welcome sequence automatically.
### Personalization
Use the synced attributes in your emails:
```
Hi {{firstName}},
Welcome to our app! We're excited to have you on board.
```
## User Lifecycle
| Clerk Event | Sequenzy Action |
| - | - |
| User created | Subscriber added, `contact.subscribed` triggered |
| User updated | Subscriber attributes updated |
| User deleted | Subscriber unsubscribed |
When a user is deleted in Clerk, they're automatically unsubscribed in Sequenzy. No manual cleanup needed.
If the same user is later re-created in Clerk (a new `user.created` event for the same email), Sequenzy reactivates the contact automatically. Explicit opt-outs (unsubscribe link, dashboard, API) are never undone by re-creation.
## Troubleshooting
### Users Not Syncing
1. Verify the webhook URL is correct in Clerk
2. Check that all required events are selected (`user.created`, `user.updated`, `user.deleted`)
3. Test by creating a new user in Clerk and checking if they appear in Sequenzy subscribers
### Invalid Signature Errors
If webhooks are failing with signature errors:
1. Verify you copied the complete signing secret from Clerk
2. Ensure the secret starts with `whsec_`
3. Try recreating the webhook in Clerk with a new secret
### Missing Profile Data
Clerk only syncs data that exists in the user profile. If `firstName` is empty in Clerk, it won't appear in Sequenzy. Ensure your Clerk sign-up flow collects the data you need.
## FAQ
No. Only users created after you connect sync automatically. Import existing Clerk users with a CSV or the API; `user.updated` only updates people who are already subscribers.
Currently, Clerk users are added to your general subscriber list. You can use
tags or automations to organize them after creation.
Users without an email address are skipped. Sequenzy requires an email to
create a subscriber.
Yes, Clerk integration is included at no extra cost.
# Creem Integration
Source: https://docs.sequenzy.com/integrations/creem
Sync revenue data from Creem to power segments and automations
Connect your Creem account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## What is Creem?
[Creem](https://creem.io) is an all-in-one payment platform that acts as a Merchant of Record. It handles global commerce complexity including payments, subscriptions, tax compliance (VAT, GST, sales tax), and regulatory requirements across 190+ countries, so you can focus on building your product.
## Connecting Creem
Creem requires manual API key and webhook setup. Here's how to connect:
### Step 1: Get Your Creem API Key
1. Go to your [Creem Dashboard](https://creem.io/dashboard)
2. Navigate to **Developers**
3. Copy the API key (format: `ck_xxxxxxxxxx`)
### Step 2: Find Your Store ID
1. In the Creem Dashboard, go to **Settings**
2. Your **Store ID** is displayed in your account settings (format: `store_xxxxxxxxxx`)
3. Copy this ID for later
### Step 3: Set Up Webhooks in Creem
1. In Creem, go to **Developers → Webhooks**
2. Click **Create Endpoint**
3. Enter your Sequenzy webhook URL:
```
https://api.sequenzy.com/api/webhooks/creem/{your-company-id}
```
You can find this URL in Sequenzy when connecting Creem. It's automatically
shown in the connection modal.
4. Select these events (or leave the default of all events on):
* `checkout.completed`
* `subscription.active`
* `subscription.paid`
* `subscription.canceled`
* `subscription.expired`
* `subscription.trialing`
* `subscription.paused`
* `refund.created`
* `dispute.created`
5. Click **Create** to save the webhook endpoint
6. Copy the **Webhook Secret** (format: `whsec_xxxxxxxxxx`)
### Step 4: Connect in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Creem
3. Enter your:
* **Store ID** (from Step 2)
* **API Key** (from Step 1)
* **Webhook Secret** (from Step 3)
4. Click **Connect Creem**
Sequenzy will immediately begin receiving webhook events from Creem.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` subscriptions count toward MRR.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Creem subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active subscription |
| `cancelled` | Subscription has been cancelled |
| `churned` | Subscription has expired or paused |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes via webhooks.
## Tracked Events
When you connect Creem, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | Checkout completed, subscription became active, or payment received |
| `saas.purchase.monthly` | Monthly subscription purchase |
| `saas.purchase.yearly` | Yearly subscription purchase |
| `saas.trial_started` | Subscription enters trial period |
| `saas.cancelled` | Subscription cancelled |
| `saas.churn` | Subscription expired or paused |
| `saas.refund` | Refund or dispute created |
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100`: High MRR customers
* `ltv > 1000`: Lifetime high-value customers
* `mrr >= 50 AND mrr < 200`: Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500`: High-value churned customers (win-back targets)
* `tag = cancelled AND mrr >= 100`: At-risk high-value customers needing attention
* `tag = saas.yearly AND mrr >= 100`: Yearly enterprise customers
### Automations
Trigger automations when revenue attributes change:
* Send a thank-you email when `ltv` exceeds a threshold
* Trigger a win-back sequence when a subscriber gets the `churned` tag
* Notify your team when a high-value customer cancels
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Creem customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If a Creem customer doesn't have an email in the webhook or doesn't match any subscriber, the event is acknowledged but no action is taken.
## Troubleshooting
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Creem settings
2. Check that you've selected the correct events
3. Check Creem's webhook logs for failed delivery attempts
4. Ensure your Sequenzy webhook endpoint is accessible
### Invalid Webhook Signature
If webhooks are failing with signature errors:
1. Verify you copied the complete webhook secret from Creem
2. Re-create the webhook endpoint in Creem and update the secret in Sequenzy
3. Ensure you're using the correct secret format (`whsec_xxxxxxxxxx`)
### Customer Email Not Found
Creem webhooks include customer data with email addresses. If events are being acknowledged but subscribers aren't being updated:
1. Ensure the customer email in Creem matches a subscriber in Sequenzy
2. Check that customers have valid email addresses in Creem
# Custom Webhooks
Source: https://docs.sequenzy.com/integrations/custom-webhooks
Trigger events directly from external services without a middleman
Connect external services like Cal.com, Typeform, Calendly, and more directly to Sequenzy without needing Zapier or other automation platforms.
## Overview
Custom webhooks allow you to trigger events in Sequenzy by sending HTTP POST requests from any service that supports webhooks. When an event is triggered, it can start sequences, apply sync rules, and more.
Each webhook gets a **unique secret URL**: no API keys or special headers needed. Just paste the URL into your external service.
**Common use cases:**
* Trigger a sequence when someone books a call via Cal.com
* Start onboarding emails when a form is submitted on Typeform
* Add subscribers when they schedule a meeting on Calendly
## Getting Started
### 1. Get Your Webhook URL
1. Open your sequence in the editor
2. Select a trigger node with an **Event** trigger (e.g., "When `call-booked` event received")
3. Find the **Webhook URL** section in the trigger configuration
4. Copy the unique URL
The URL format is:
```
https://api.sequenzy.com/api/inbound-webhooks/{secret}
```
The secret in the URL acts as authentication: no API key headers needed.
### 2. Send a Test Webhook
Paste the URL into your external service and send a test request. Sequenzy will capture the raw payload.
Alternatively, you can paste an example payload directly in the Sequenzy UI.
### 3. Map Fields
Once a payload is captured, Sequenzy shows you the raw JSON and lets you map fields:
* **Email** (required): e.g., `payload.attendees[0].email`
* **First name**: e.g., `payload.attendees[0].name`
* **Last name**: optional
* **Custom properties**: e.g., `payload.duration` → `duration`
Choose each source path from its mapping dropdown. The options come from the captured payload, including nested array values such as `payload.attendees[0].email`. Email is required; the profile fields and custom event properties are optional.
### 4. Save and Activate
Save the mapping. Future webhooks will automatically extract the fields and trigger the event.
## How It Works
The webhook has three states:
1. **Waiting for payload**: URL is generated, waiting for the first request
2. **Payload captured**: Raw payload stored, ready for field mapping
3. **Active**: Mapping configured, future requests trigger events automatically
When a webhook is active and receives a request:
1. Fields are extracted from the payload using dot-notation paths (supports array indexing like `payload.items[0].email`)
2. The subscriber is created if they don't exist
3. The event is triggered, which can start sequences
## Service-Specific Setup
### Cal.com
1. In Cal.com, go to your event type > **Webhooks**
2. Click **New Webhook** and paste your Sequenzy webhook URL in the **Subscriber URL** field
3. Leave the **Secret** field empty (it's for HMAC signature verification, not needed here: your URL already contains a unique secret)
4. Select the events you want to trigger (e.g., `BOOKING_CREATED`)
5. Send a test: Sequenzy captures the payload
6. Map fields: `payload.attendees[0].email` → Email, `payload.attendees[0].name` → First Name
### Typeform
1. In Typeform, go to **Connect** > **Webhooks**
2. Add your Sequenzy webhook URL
3. Submit a test response
4. Map fields from the captured payload (e.g., `form_response.answers[0].email` → Email)
### Calendly
1. In Calendly, go to **Integrations** > **Webhooks**
2. Add a webhook with your Sequenzy URL
3. Book a test meeting
4. Map fields: `payload.invitee.email` → Email, `payload.invitee.name` → First Name
### Generic Webhook Sources
For any service that supports webhooks:
1. Find the webhook configuration in the service
2. Paste the Sequenzy webhook URL as the destination
3. Send a test request
4. Map the fields in Sequenzy
## Event Properties
You can map custom properties from the payload to event properties. These are stored with the event and can be used for:
* Filtering in sync rules
* Personalization in emails (for example, `Thanks for booking a {{duration}}-minute call!`)
## Managing Webhooks
### Edit Mapping
Click "Edit mapping" on an active webhook to change the field mapping.
### Reset
Click "Reset" to clear the mapping and captured payload, returning to the "waiting for payload" state.
### Regenerate URL
If your webhook URL is compromised, click "Regenerate URL" to create a new secret. The old URL will stop working immediately.
Regenerating the URL will break existing integrations. You'll need to update
the URL in your external service.
## Best Practices
1. **Send a real test request** from your external service before going live. This captures the actual payload format.
2. **Use descriptive event names** like `call-booked`, `form-submitted`, or `demo-scheduled` rather than generic names.
3. **Map all relevant properties** that you might want to use for personalization or filtering later.
4. **Handle errors gracefully**: the webhook returns 200 even for processing errors to prevent external services from retrying unnecessarily.
## Troubleshooting
### Event Not Triggering Sequence
1. Verify the webhook status is "Active" (green badge)
2. Check that the email mapping extracts a valid email from the payload
3. Ensure the sequence is published and active
4. Verify the subscriber meets any conditions on the sequence
### Mapping Not Working
1. Check that the field paths are correct (use dot notation, e.g., `payload.attendees[0].email`)
2. Verify the payload structure matches what you mapped: send another test webhook to update the sample
3. Ensure the email field extracts a valid email address (must contain `@`)
### Webhook URL Not Working
1. Verify you're using the correct URL (it should start with `https://api.sequenzy.com/api/inbound-webhooks/`)
2. If you regenerated the URL, update it in your external service
3. Ensure the request uses POST method with a JSON, form-encoded, or multipart body
## FAQ
No! Custom webhooks let you connect directly to Sequenzy without any
middleman service.
No. The webhook URL contains a unique secret that acts as authentication. No
headers needed.
Any JSON object or array, a form-encoded body
(`application/x-www-form-urlencoded`), or a multipart form. Sequenzy
captures whatever your external service sends and lets you map the fields to
subscriber data. Payloads are limited to 1 MB, and file uploads in multipart
forms are ignored.
Each webhook call creates a new event. If your sequence is set to enroll
subscribers only once, they won't re-enter the sequence.
Yes, webhooks are available on all plans.
Standard API rate limits apply. For high-volume scenarios, consider using
our bulk API endpoints.
Yes! In the sequence editor, ask the AI chat to help you connect external
services like Cal.com or Typeform. It will guide you through the webhook
configuration.
# Data Exports
Source: https://docs.sequenzy.com/integrations/data-exports
Stream email events, SMS events, custom events and daily subscriber snapshots to your S3 or Google Cloud Storage bucket, ready for Snowflake, BigQuery, Redshift and more.
Data exports write your Sequenzy data to a bucket you own, every few minutes, as gzipped JSON lines. Load the files into Snowflake, BigQuery, Redshift, Databricks, Athena or DuckDB and join email engagement with the rest of your product data.
You can set up an export in **Settings → Data Warehouse → Export to bucket**, with the [API](/api-reference/data-exports/create), the [CLI](/concepts/cli) (`sequenzy data-exports create`) or the [MCP server](/concepts/mcp) (`create_data_export`).
Need events in real time instead of in batches? Use [outbound
webhooks](/integrations/outbound-webhooks). Data exports are built for
analytics: complete history, stable files, and no endpoint to keep online.
## Datasets
| Dataset | What it contains | How it is written |
| - | - | - |
| `email_events` | Sends, deliveries, delays, bounces, complaints, opens, clicks, unsubscribes and transport failures | New rows every run |
| `sms_events` | SMS sends, deliveries, failures, clicks, opt-outs and replies | New rows every run |
| `custom_events` | Events you track through the API, SDKs and integrations | New rows every run |
| `subscribers` | Every subscriber with status, tags and custom attributes | One full snapshot per UTC day |
Test sends are never exported. Events imported as history (for example through [Import Events](/api-reference/subscribers/events/import) with timestamps older than an hour) are your own backfilled data and are not streamed back out.
## Supported storage
* **Amazon S3.** Provide the bucket, region and an access key.
* **Google Cloud Storage.** Create an [HMAC key](https://cloud.google.com/storage/docs/authentication/hmackeys) for a service account with the **Storage Object Creator** role on the bucket, and choose `gcs` as the provider.
* **S3-compatible storage** such as Cloudflare R2, MinIO or Backblaze B2. Choose `s3` and set the endpoint, for example `https://.r2.cloudflarestorage.com`. The endpoint must use HTTPS and resolve to a public address.
The key only needs permission to write objects under your path prefix. Here is a minimal AWS policy:
```json theme={null}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::acme-exports/sequenzy/*"
}
]
}
```
When you save a destination, Sequenzy writes `/_sequenzy/connection-test.json` to confirm access. If that fails, nothing is saved and you see the reason, such as a missing bucket, a wrong region or a denied write. Secrets are stored encrypted and are never shown again.
## File layout
```text theme={null}
/email_events/date=2026-09-30/email_events---0000.jsonl.gz
/sms_events/date=2026-09-30/sms_events---0000.jsonl.gz
/custom_events/date=2026-09-30/custom_events---0000.jsonl.gz
/subscribers/date=2026-09-30/subscribers-0000.jsonl.gz
/subscribers/date=2026-09-30/_SUCCESS
```
* Each file is gzip-compressed JSON, one object per line, up to 50,000 lines.
* `date=` folders are Hive-style partitions, so warehouses can prune by day.
* Event files cover a window of **recorded time** (`fromMs` exclusive, `toMs` inclusive, in epoch milliseconds). A delayed bounce lands in the window in which Sequenzy recorded it, so nothing is missed behind an earlier window.
* The subscriber snapshot is complete when `_SUCCESS` exists. Load a day's snapshot only after that marker appears.
## Delivery guarantees
* **At least once.** If a run fails partway, the retry rewrites the same window with the same file names. In rare cases, such as a changed file count after a retry, a row can appear twice. Deduplicate on `id`.
* **No gaps.** A window only advances after every file in it is written. Exports trail real time by about two minutes so late writes are included.
* **Retries.** Failed runs retry automatically, backing off from the export frequency up to every 6 hours. The dashboard, API and CLI show the last error until a run succeeds.
* **Ordering.** Rows inside a file are ordered by recorded time. Use `occurred_at` for analysis.
The first export includes events recorded after you create the destination. You can include up to 30 days of earlier events with the **First export** option (`startFrom` in the API). Events recorded more than 7 days after they occurred are not part of the stream.
## Row schemas
Every row has `schema_version` (currently `1`). New fields may be added in the same version; existing fields do not change meaning.
### email\_events
| Field | Description |
| - | - |
| `id` | Event ID. Use it to deduplicate. |
| `event_type` | `sent`, `delivered`, `delivery_delayed`, `bounced`, `complained`, `opened`, `clicked`, `unsubscribed` or `failed` |
| `occurred_at` / `recorded_at` | When it happened, and when Sequenzy recorded it (ISO 8601, UTC) |
| `subscriber_id`, `email` | Recipient |
| `email_send_id` | One ID per message. Joins every event of the same email. |
| `message_type` | `campaign`, `sequence` or `transactional` |
| `campaign_id` / `sequence_step_id` | The campaign, or the sequence step, that sent the email |
| `transactional_email_id`, `email_name` | Transactional template ID and the email's subject or name |
| `ab_test_id`, `ab_test_variant_id` | A/B test attribution |
| `bounce_type`, `bounce_sub_type` | For `bounced`: `Permanent` or `Transient` and the detail |
| `complaint_type` | For `complained` |
| `clicked_url` | For `clicked` |
| `ip_address`, `user_agent`, `country_code` | For opens and clicks, when available |
| `recipient_domain`, `mailbox_provider` | For example `gmail.com` and `gmail` |
### sms\_events
`id`, `event_type` (`sent`, `delivered`, `failed`, `clicked`, `opted_out`, `received`), `occurred_at`, `recorded_at`, `subscriber_id`, `sms_send_id`, `campaign_id`, `sequence_step_id`, `segments`, `credits`, `error_code`, `clicked_url`.
### custom\_events
`id`, `event_name`, `occurred_at`, `recorded_at`, `subscriber_id`, `properties` (object), `campaign_id`, `sequence_step_id`.
### subscribers
`snapshot_date`, `id`, `email`, `external_id`, `first_name`, `last_name`, `phone`, `status`, `sms_status`, `tags` (array), `custom_attributes` (object), `created_at`, `updated_at`.
The snapshot reads subscribers in pages while you keep working, so a contact changed during the export may show either version. Deleted subscribers are absent from the next snapshot.
## Load into your warehouse
```sql theme={null}
CREATE STAGE sequenzy_stage
URL = 's3://acme-exports/sequenzy/'
STORAGE_INTEGRATION = my_s3_integration
FILE_FORMAT = (TYPE = JSON COMPRESSION = GZIP);
CREATE TABLE email_events (raw VARIANT, file STRING);
-- Run on a schedule, or wrap in a Snowpipe with AUTO_INGEST = TRUE
COPY INTO email_events (raw, file)
FROM (SELECT $1, METADATA$FILENAME FROM @sequenzy_stage/email_events/);
SELECT raw:event_type::string AS event_type, COUNT(DISTINCT raw:id)
FROM email_events
GROUP BY 1;
```
```sql theme={null}
CREATE EXTERNAL TABLE sequenzy.email_events_raw
WITH PARTITION COLUMNS (date DATE)
OPTIONS (
format = 'NEWLINE_DELIMITED_JSON',
compression = 'GZIP',
uris = ['gs://acme-exports/sequenzy/email_events/*'],
hive_partition_uri_prefix = 'gs://acme-exports/sequenzy/email_events'
);
SELECT event_type, COUNT(DISTINCT id)
FROM sequenzy.email_events_raw
WHERE date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
GROUP BY event_type;
```
```sql theme={null}
CREATE TABLE email_events_staging (
id VARCHAR(128), event_type VARCHAR(32), occurred_at TIMESTAMPTZ,
subscriber_id VARCHAR(128), email VARCHAR(255), campaign_id VARCHAR(128),
clicked_url VARCHAR(2048)
);
COPY email_events_staging
FROM 's3://acme-exports/sequenzy/email_events/date=2026-09-30/'
IAM_ROLE 'arn:aws:iam::123456789012:role/redshift-s3-read'
FORMAT AS JSON 'auto ignorecase'
GZIP TIMEFORMAT 'auto';
```
```sql theme={null}
SELECT event_type, COUNT(DISTINCT id)
FROM read_json_auto('s3://acme-exports/sequenzy/email_events/*/*.jsonl.gz',
hive_partitioning = true)
GROUP BY event_type;
```
## Manage exports
* **Pause and resume.** Paused exports keep their position and continue where they stopped. Pausing or deleting an export stops a run in progress before its next file.
* **Change datasets.** Datasets you add start from the time you add them.
* **Move or rotate keys.** Changing the bucket, prefix or credentials writes a new test file first. Exports continue from their position in the new location, and a subscriber snapshot in progress starts over there. An export already running stops before its next file and continues with the new settings.
* **Export now.** Runs immediately instead of waiting for the schedule.
* **History.** Each run from the last 14 days shows rows, files, size and any error.
* **Delete.** Stops the export. Files already written stay in your bucket.
Only owners and admins can manage data exports. API keys need `data_exports:read` to view them, and `data_exports:write` plus `subscribers:read` and `analytics:read` to create, change, test or run them, and `data_exports:delete` to delete them.
# Dodo Payments Integration
Source: https://docs.sequenzy.com/integrations/dodo
Sync revenue data from Dodo Payments to power segments and automations
Connect your Dodo Payments account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## What is Dodo Payments?
[Dodo Payments](https://dodopayments.com) is a simple payments platform designed for indie developers and small businesses. It offers straightforward pricing, easy integration, and handles payments globally with minimal setup.
## Connecting Dodo Payments
Dodo Payments requires manual API key and webhook setup. Here's how to connect:
### Step 1: Get Your Dodo API Key
1. Go to your [Dodo Payments Dashboard](https://dashboard.dodopayments.com)
2. Navigate to **Settings → API Keys & SMTP**
3. Create a new API key or copy an existing one
4. Copy the API key
Your API key is only shown once. Store it securely before closing the dialog.
### Step 2: Find Your Business ID
1. In the Dodo Payments Dashboard, go to **Settings**
2. Your **Business ID** is displayed in your account settings (format: `bus_xxxxxxxxx`)
3. Copy this ID for later
### Step 3: Set Up Webhooks in Dodo Payments
1. In Dodo Payments, go to **Settings → Webhooks**
2. Click **Add Endpoint**
3. Enter your Sequenzy webhook URL:
```
https://api.sequenzy.com/api/webhooks/dodo/{your-company-id}
```
You can find this URL in Sequenzy when connecting Dodo Payments - it's
automatically shown in the connection modal.
4. Select these events to listen for:
* `payment.succeeded`
* `payment.failed`
* `payment.cancelled`
* `subscription.active`
* `subscription.updated`
* `subscription.renewed`
* `subscription.plan_changed`
* `subscription.cancelled`
* `subscription.expired`
* `subscription.on_hold`
* `subscription.failed`
* `refund.succeeded`
5. Click **Create** to save the webhook endpoint
6. Copy the **Webhook Secret** (format: `whsec_xxxxxxxxx`)
### Step 4: Connect in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Dodo Payments
3. Enter your:
* **Business ID** (from Step 2)
* **API Key** (from Step 1)
* **Webhook Secret** (from Step 3)
4. Click **Connect Dodo Payments**
Sequenzy will immediately begin receiving webhook events from Dodo Payments.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` subscriptions count toward MRR.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Dodo Payments subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active subscription |
| `cancelled` | A live subscription is scheduled to end at its next billing date (`cancel_at_next_billing_date`) |
| `churned` | The subscription has ended: `subscription.cancelled` with status `cancelled`, or `subscription.expired` |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes via webhooks.
Dodo sends `subscription.cancelled` when a subscription actually ends, whether
the customer cancelled immediately or a scheduled cancellation reached its
billing date. A scheduled cancellation arrives as `subscription.updated` with
`cancel_at_next_billing_date` set, and un-scheduling it removes the
`cancelled` tag again. Both paths match what a manual **Sync revenue** run
produces from the Dodo API.
## Tracked Events
When you connect Dodo Payments, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | Payment succeeded (`payment.succeeded`) |
| `saas.purchase.monthly` | Monthly subscription purchase |
| `saas.purchase.yearly` | Yearly subscription purchase |
| `saas.cancelled` | Live subscription scheduled to end at its next billing date |
| `saas.churn` | Subscription ended (`subscription.cancelled` or `subscription.expired`) |
| `saas.payment_failed` | Payment failed or subscription on hold/failed |
| `saas.refund` | Refund succeeded |
| `saas.checkout_expired` | Payment cancelled (abandoned checkout) |
Subscription lifecycle events keep subscription attributes up to date without
firing a separate purchase event. `subscription.plan_changed` also updates MRR
without increasing LTV. Dodo's `payment.succeeded` event updates
payment-driven MRR and LTV and triggers `saas.purchase`.
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100`: High MRR customers
* `ltv > 1000`: Lifetime high-value customers
* `mrr >= 50 AND mrr < 200`: Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500`: High-value churned customers (win-back targets)
* `tag = cancelled AND mrr >= 100`: At-risk high-value customers needing attention
* `tag = saas.yearly AND mrr >= 100`: Yearly enterprise customers
### Automations
Trigger automations when revenue attributes change:
* Send a thank-you email when `ltv` exceeds a threshold
* Trigger a win-back sequence when a subscriber gets the `churned` tag
* Notify your team when a high-value customer cancels
## Which Lists Contacts Join
Dodo Payments has no per-integration list targeting. Customers it creates,
including the initial revenue backfill, join your workspace
[Default Lists](/concepts/subscribers#default-lists).
A JSON `null` on that setting means **every current and future list**, not "no
lists". `[]` is the value that joins nothing. Check it before connecting, and
set it to specific lists (or to none) if you do not want paying customers in
every marketing list:
```bash theme={null}
sequenzy companies get comp_abc123
sequenzy companies update comp_abc123 --default-list list_customers
```
Over MCP the same field is `defaultSubscriberListIds` on `get_company` /
`update_company`. Changing the default only affects later writes and also
repoints forms, imports, and every other untargeted source. Stripe can be
pointed at its own lists with `set_integration_list_targeting`; Dodo cannot.
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Dodo Payments
customer with email `User@Example.com` will match a subscriber with email
`user@example.com`.
A customer with an email that is not already in Sequenzy is created as a new
subscriber and joined to the workspace Default Lists. Events with no email are
acknowledged and skipped.
## Troubleshooting
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Dodo Payments settings
2. Check that you've selected the correct events
3. Check Dodo Payments webhook logs for failed delivery attempts
4. Ensure your Sequenzy webhook endpoint is accessible
### Invalid Webhook Signature
If webhooks are failing with signature errors:
1. Verify you copied the complete webhook secret from Dodo Payments
2. Re-create the webhook endpoint in Dodo Payments and update the secret in Sequenzy
3. Ensure you're using the correct secret format
### Customer Email Not Found
Dodo Payments webhooks include customer data with email addresses. A customer
with a valid email is created if they are not already a subscriber. If events
are acknowledged but nobody appears:
1. Confirm the webhook payload includes a customer email - events without one are skipped
2. Check that the address in Dodo Payments is a valid email
3. Read workspace [Default Lists](/concepts/subscribers#default-lists) if the
contact exists but is missing from the lists your segments use - a JSON
`null` there means every list, `[]` means none
# Framer Integration
Source: https://docs.sequenzy.com/integrations/framer
Collect Framer signups with the Sequenzy Signup component or a native Framer form webhook.
You can collect Framer signups in Sequenzy with the published Framer component
or with Framer's native Form component.
## Sequenzy Signup Component
Use the Sequenzy Signup component when you want a ready-to-use Framer form with
Sequenzy settings exposed in Framer's right panel.
Create or open the Sequenzy form you want to reuse first. The template gallery
is where new saved forms start:
The component supports:
* Form ID connection
* Workspace-default lists, no-list capture, or specific list IDs
* Existing Sequenzy tag IDs
* Inline or stacked layouts
* Editable placeholder, button text, success message, colors, radius, font
size, and redirect URL
The component submits to Sequenzy's public form endpoint:
```txt theme={null}
https://api.sequenzy.com/api/v1/forms/{companyId}
```
For this component, the Sequenzy **Form ID** is the public form/company ID used
by the existing signup form endpoint.
## Install From Framer
1. Open your Framer project.
2. Open the
[Sequenzy Signup component](https://www.framer.com/marketplace/components/sequenzy/)
in the Framer Marketplace.
3. Copy the component into your project.
4. Select the inserted component on the canvas.
5. Paste your Sequenzy Form ID into the Framer panel.
6. Choose list targeting:
* **Workspace default** uses your Sequenzy form defaults.
* **Specific IDs** sends subscribers to selected list IDs.
* **No lists** creates the subscriber without list membership.
7. Optionally enter tag IDs to apply existing Sequenzy tags.
8. Set placeholder text, button text, colors, layout, and redirect URL.
9. Publish your Framer site and submit a test email.
## Native Framer Form
You can also use Framer's built-in Form component when you want full control
over fields, layout, validation, and success behavior.
1. In Framer, add a Form component.
2. Set the email field name to `email`.
3. Set **Send To** to **Webhook**.
4. Paste your Sequenzy form endpoint:
```txt theme={null}
https://api.sequenzy.com/api/v1/forms/{companyId}
```
Optional field names:
* `firstName`
* `lastName`
* `customAttributes[nickname]`, `customAttributes[zipcode]`, or another custom attribute key
* `redirectUrl`
* `website` as a hidden honeypot field
List and tag routing can be added to the endpoint URL:
```txt theme={null}
https://api.sequenzy.com/api/v1/forms/company_123?lists=list_abc,list_def&tags=tag_abc
```
Use `lists=` to create subscribers without list membership. Omit `lists` to use
workspace default list targeting.
## Test The Integration
1. Publish your Framer site.
2. Submit the form with a test email address.
3. Confirm the subscriber appears in Sequenzy.
4. If you configured list IDs, confirm the subscriber is attached to those
lists.
5. If you configured tag IDs, confirm the tags are applied.
6. If you configured a redirect URL, confirm the published site redirects after
a successful submission.
## Troubleshooting
If submissions fail:
* Confirm the Form ID is a valid Sequenzy company/form ID.
* Confirm optional list IDs and tag IDs belong to the same Sequenzy workspace.
* Use `lists=` only when you intentionally want no list membership.
* Keep the hidden `website` honeypot field empty.
* Test from the published Framer site, not only from the Framer editor canvas.
## References
* [Sequenzy Signup on Framer Marketplace](https://www.framer.com/marketplace/components/sequenzy/)
* [Signup Form Widget](/widgets/signup-form)
* [Submit Form API](/api-reference/widgets/submit-form)
# Lemon Squeezy Integration
Source: https://docs.sequenzy.com/integrations/lemon-squeezy
Sync Lemon Squeezy customers, subscriptions, and completed revenue into Sequenzy
Connect one Lemon Squeezy store to a Sequenzy workspace to sync billing state,
MRR, LTV, and subscription lifecycle events. Your existing Lemon Squeezy
checkout remains the source of purchases; Sequenzy does not create a separate
checkout or product catalog.
## Connect your store
You need a Lemon Squeezy API key and the numeric store ID.
1. Open **Settings → Integrations → Lemon Squeezy** in Sequenzy.
2. Enter the API key and store ID.
3. Leave **Manual webhook setup** off for the recommended managed setup.
4. Select **Connect Lemon Squeezy**.
Sequenzy validates the key, confirms access to the requested store, detects
live or test mode, creates a signed webhook, and then queues the initial
revenue sync. API keys expire or can be rotated; reconnect the integration
after replacing a key.
Lemon Squeezy keeps live and test data separate. A test-mode key creates a
test-mode webhook and syncs only test data.
### Manual webhook fallback
If the API key cannot manage webhooks, enable **Manual webhook setup** and
provide your own 16-40 character signing secret. Create the webhook in Lemon
Squeezy with the URL returned by Sequenzy:
```text theme={null}
https://api.sequenzy.com/api/webhooks/lemon_squeezy/{companyId}
```
Subscribe it to exactly these events:
```text theme={null}
order_created
order_refunded
customer_updated
subscription_created
subscription_updated
subscription_cancelled
subscription_resumed
subscription_expired
subscription_paused
subscription_unpaused
subscription_payment_success
subscription_payment_failed
subscription_payment_recovered
subscription_payment_refunded
```
Reconnecting a managed integration rotates its signing secret. Disconnecting
deletes only webhooks managed by Sequenzy; manual webhooks remain under your
control.
## Synced data
The initial sync walks every page of customers, subscriptions, orders, and
subscription invoices. Only Price and Variant details referenced by a
subscription are fetched; Sequenzy does not mirror the complete Lemon Squeezy
catalog.
Subscribers are matched by email and receive `mrr`, `ltv`, billing tags, and:
| Attribute | Description |
| - | - |
| `lemonSqueezyCustomerId` | Lemon Squeezy customer ID |
| `lemonSqueezySubscriptionId` | Most recently changed live subscription ID |
| `lemonSqueezyProductId` | Current product ID |
| `lemonSqueezyVariantId` | Current variant ID |
| `lemonSqueezyPriceId` | Current Price resource ID |
| `lemonSqueezyPlanName` | Current product or variant name |
| `lemonSqueezyInterval` | Day, week, month, or year |
| `lemonSqueezyStatus` | Latest subscription status |
| `lemonSqueezyRenewsAt` | Next renewal timestamp |
| `lemonSqueezyEndsAt` | End of a cancellation grace period |
Multiple subscriptions remain independent when MRR is calculated. The scalar
plan fields describe the most recently changed live subscription.
## Revenue calculation
MRR normalizes standard recurring prices across daily, weekly, monthly, and
yearly intervals. Usage-based and tiered subscriptions cannot be projected
from a fixed unit price, so Sequenzy uses the latest paid subscription invoice
as a trailing MRR estimate.
LTV includes successful orders and subscription invoices, net of refunds.
Lemon Squeezy emits companion events for the first subscription payment and for
payment recovery; Sequenzy deduplicates those events. Partial refunds reduce
LTV only by the increase in the provider's cumulative refunded amount.
## Lifecycle behavior
* `cancelled` retains access and MRR through `ends_at`; expiration emits
`saas.churn` and removes MRR.
* `past_due` remains live while Lemon Squeezy retries payment.
* `unpaid` and `paused` remove MRR without emitting terminal churn.
* Resume or unpause restores the subscription state without inventing a
purchase.
* A recurring amount change on `subscription_updated` emits `saas.upgrade` or
`saas.downgrade` based on the prior normalized amount.
* Successful charges emit `saas.purchase`, plus monthly or yearly purchase
classification when applicable. Failures and refunds emit
`saas.payment_failed` and `saas.refund`.
* Customer updates refresh profile data without triggering an automation.
Lemon Squeezy does not expose an abandoned-checkout webhook event, so this
integration cannot emit `saas.checkout_expired`.
## Delivery, retries, and limits
Webhook signatures are verified from the raw request body with Lemon
Squeezy's `X-Signature` HMAC-SHA256 scheme. Valid deliveries are queued before
subscriber updates run. Identical payloads, provider retries, and companion
events are idempotent.
The Lemon Squeezy API is limited to 300 requests per minute. Sequenzy requests
100 records per page and applies bounded backoff to rate limits and transient
failures. Lemon Squeezy retries webhook deliveries when Sequenzy returns a
non-success response.
See Lemon Squeezy's [API documentation](https://docs.lemonsqueezy.com/api),
[webhook event list](https://docs.lemonsqueezy.com/help/webhooks/event-types),
and [testing guide](https://docs.lemonsqueezy.com/guides/developer-guide/testing-going-live).
## Disconnect and retry cleanup
Use Settings → Integrations, `sequenzy integrations disconnect `, MCP
`disconnect_integration`, or [Disconnect Integration](/api-reference/integrations/disconnect).
Disconnection stops ingestion and fences in-flight syncs before removing the
managed webhook. Your subscribers and history remain. Manual webhooks are not
deleted. If `cleanupWarning` is non-null, local disconnection has succeeded;
repeat disconnect to retry provider cleanup. Reconnect with valid credentials
to resume ingestion.
# Make Integration
Source: https://docs.sequenzy.com/integrations/make
Use Sequenzy subscribers, lists, campaigns, email, and events in Make scenarios
Connect Sequenzy to Make scenarios to react to subscriber, email, SMS, campaign, and sequence activity, manage contacts and lists, record events, send email, enroll contacts in sequences, schedule campaigns, and report on results.
The connector is currently available as a private Make custom app while its
public Marketplace review is completed. Your workspace administrator must
install the custom app before you can create a Sequenzy connection.
## Connect a workspace
The Make connector accepts only a workspace-scoped API key:
1. Open **Settings → API Keys & SMTP** in Sequenzy.
2. Create a key for the Make scenario. New workspace keys start with `seq_live_`.
3. Select the permissions the scenario needs.
4. Create a Sequenzy connection in Make and paste the key.
Account-wide `seq_user_` keys are intentionally rejected. A Make connection stays bound to one workspace, preventing a scenario from silently operating on a different workspace.
| Scenario capability | Required permissions |
| - | - |
| Connect | `account:read` |
| Read subscribers, notes, and imports | `subscribers:read` |
| Create, update, unsubscribe, or import | `subscribers:write` |
| Create or update a subscriber with lists | `subscribers:write`, `lists:read` |
| Import subscribers into lists | `subscribers:write`, `lists:read`, `lists:write` |
| Permanently delete subscribers | `subscribers:delete` |
| Tag one subscriber | `subscribers:write` |
| Bulk tag or untag many subscribers | `subscribers:tag`, plus `tags:write` for a tag that does not exist yet |
| Read or create tags | `tags:read`, `tags:write` |
| Read or create lists | `lists:read`, `lists:write` |
| Add or remove list members | `lists:read`, `lists:write`, `subscribers:write` |
| Read segments and counts | `segments:read` |
| Record subscriber events | `subscribers:write`, `events:write` |
| Start automations from writes or events | `automations:trigger` |
| Use workspace-default or double opt-in | `automations:trigger` when a write could send a confirmation email |
| Read sequences | `sequences:read` |
| Enroll or cancel sequence enrollments | `sequences:read`, `sequences:enroll` |
| Read campaigns | `campaigns:read` |
| Schedule or test-send campaigns | `campaigns:read`, `campaigns:send` |
| Get campaign metrics | `campaigns:read`, `analytics:read` |
| Read email sends and suppressions | `analytics:read` |
| Send transactional or marketing email | `transactional:send`, plus `transactional:read` for template choices |
| Push orders and checkouts | `commerce:write` |
| Any instant trigger | `webhooks:write`, `webhooks:delete` |
A valid key that lacks a permission produces the API's specific 403 message in Make, not a generic connection error.
Module dropdowns load live workspace data. Normal setup also needs the matching read scope for every dropdown shown: `lists:read`, `tags:read`, `segments:read`, `sequences:read`, `campaigns:read`, or `transactional:read`. This matters even when the action itself uses a write, enroll, or send scope.
## Triggers
Every trigger is instant. Adding it registers a Sequenzy outbound webhook scoped to the events you select, and deleting the trigger removes that endpoint.
| Trigger | Starts the scenario when |
| - | - |
| **Watch subscriber activity** | A contact is created, updated, unsubscribed, joins or leaves a list, or is reported invalid |
| **Watch email activity** | An email is sent, delivered, delayed, bounced, fails, is marked as spam, opened, clicked, or unsubscribed from |
| **Watch email replies** | A recipient replies to a tracked email |
| **Watch poll answers** | A recipient answers a poll or NPS block |
| **Watch SMS activity** | An SMS is sent, delivered, fails, or a contact opts out |
| **Watch sent campaigns** | A campaign finishes sending, with its final counts |
| **Watch finished sequences** | A contact finishes a sequence, or a sequence run fails |
| **Watch finished subscriber imports** | An import completes, with reconciled counts and failure reasons |
Select only the event types your scenario needs - opens and clicks are high volume on large sends. If the scenario writes back to Sequenzy, add a filter so its own writes do not retrigger it.
## Actions and searches
**Subscribers**
* **Get a subscriber** and **Get a subscriber by external ID** return one contact's profile, lists, sequence enrollments, notes, email statistics, and recent activity. A missing contact returns a 404 error you can handle with an error route.
* **Search subscribers** returns one bundle per contact, filtered by status, search term, tags, list, segment, custom attribute, or opt-out window.
* **Create or update a subscriber** creates a contact or updates the fields mapped in the scenario. Its existing-subscriber behavior can instead fill only empty fields or skip profile changes.
* **Add a note to a subscriber** records an internal note on the profile.
* **Unsubscribe a subscriber** performs the full opt-out workflow while retaining the contact record.
* **Delete a subscriber** permanently deletes the contact. Use unsubscribe for normal customer opt-outs.
* **Import subscribers** queues up to 5,000 full records; **Get a subscriber import** reports progress and per-reason failures. Selecting lists needs `lists:read` and `lists:write`. Workspace-default opt-in needs `automations:trigger` when any imported record is active or has no mapped status; double opt-in always needs it. Use confirmed opt-in only for consent verified outside Sequenzy.
**Tags, lists, and segments**
* **Add a tag to a subscriber** and **Remove a tag from a subscriber** work on one contact and create it when necessary.
* **Add tags to many subscribers** and **Remove tags from many subscribers** reconcile up to 25 tags across 500 existing contacts without creating anyone.
* **List tags** and **Create a tag** manage tag definitions and their dashboard colors.
* **List subscriber lists**, **Create a subscriber list**, **Add subscribers to a list**, and **Remove subscribers from a list** manage list membership. Removing membership never deletes the contact.
* **List segments** and **Get a segment count** read saved segments and their current size.
**Events, sequences, and campaigns**
* **Create a trigger event** records a live or historical event using a required idempotency key.
* **Search sequences**, **Enroll subscribers in a sequence**, and **Cancel sequence enrollments** drive automations from outside Sequenzy.
* **Search campaigns**, **Schedule a campaign**, **Send a campaign test**, and **Get campaign metrics** cover the send and reporting loop.
**Email, deliverability, and commerce**
* **Send an email** queues a transactional or single-recipient marketing email from a saved template or direct HTML.
* **Get an email send** returns the delivery snapshot and recorded events for one send ID.
* **Check a suppressed recipient** and **Search suppressed recipients** show who Sequenzy cannot reach and why.
* **Push an order** and **Track a started checkout** feed commerce automations such as order confirmation, replenishment, and abandoned checkout.
* **Make an API call** reaches any other authenticated Sequenzy endpoint when no dedicated module exists.
Existing private-app scenarios that used the former `findSubscriber` module remain compatible. The deployment helper hides that retired module without deleting it; use **Get a subscriber** or **Search subscribers** when building new scenarios.
## Choosing values without pasting IDs
Lists, tags, segments, sequences, campaigns, and transactional templates load as live dropdowns from your workspace. Each dropdown is still mappable, so a scenario can supply an ID from an earlier module instead.
## Retry-safe writes
Make retries a failed step, so every module that could duplicate work exposes a way to stay idempotent:
* **Create a trigger event** requires an **Idempotency key**. Map a stable source ID such as a webhook, invoice, or order ID. Historical retries with the same subscriber, event name, and key are strictly deduplicated. Live-event deduplication is best-effort for nearly simultaneous requests, so a producer that needs a strict guarantee should also keep its own source ledger. The output reports `duplicate` for a repeated live event and exposes any `sideEffectFailures`. When `occurredAt` is more than one hour in the past, the event is imported as history and the output reports `historical`, `inserted`, and `duplicates`.
* **Send an email** accepts an **Idempotency key**. Reusing the same key and content returns the original send for 14 days instead of delivering twice.
* **Import subscribers** requires an **Idempotency key** and reports `deduplicated` when a retry reuses the queued import.
* **Push an order** deduplicates on your order ID, so pushing the same order twice never double counts revenue.
* **Delete a subscriber** succeeds with `alreadyMissing: true` when the contact is already gone.
## Partial results
Bulk modules report per-record outcomes instead of failing the whole scenario:
* **Add tags to many subscribers** and **Remove tags from many subscribers** expose `matched`, `updated`, `failed`, `failures`, and `notFound`.
* **Add subscribers to a list** exposes per-email `results` alongside the counts.
* **Enroll subscribers in a sequence** reports `enrolled`, `skipped`, and `notFound`.
* **Cancel sequence enrollments** is capped at 1,000 enrollments per call. Repeat the module while `remainingCount` is above zero.
## Rate limits
The connector converts HTTP 429 responses into Make rate-limit errors, allowing Make to delay and retry the scenario. Current API limits and response headers are documented in [Rate Limiting](/api-reference/introduction#rate-limiting).
## Reading large audiences
**Search subscribers** follows stable cursor pages automatically and returns up to the requested Make limit. Cursor pagination keeps results stable while contacts are created or deleted mid-pull.
## Campaign audiences
**Schedule a campaign** sends to the lists you select, or reuses the campaign's saved audience when you leave the list field empty - which falls back to every active subscriber if the campaign has no saved targeting. Segment and rule audiences are configured on the campaign in Sequenzy, or through **Make an API call** with a `targetLists` object.
A campaign held for safety review returns a normal success with `waitingApproval: true` and status `waiting_approval`. Retrying the schedule step does not clear the hold, so branch on the status instead of retrying.
# Meta Ads Integration
Source: https://docs.sequenzy.com/integrations/meta-ads
Sync segments to Meta custom audiences for Facebook and Instagram retargeting
Push any Sequenzy segment to a Meta custom audience and keep it updated on a schedule. Run paid social campaigns on Facebook and Instagram that target (or exclude) exactly the same people as your email campaigns.
## How it works
1. You map a segment to a custom audience in one of your Meta ad accounts
2. Sequenzy uploads the segment's active subscribers to the audience on your chosen schedule
3. Meta matches the uploaded emails to Facebook and Instagram accounts
4. You use the audience in Meta Ads Manager for retargeting, exclusions, or as the seed for a lookalike audience
Emails are SHA-256 hashed before they reach Meta, per Meta's customer list requirements.
## Connecting Meta Ads
1. Go to **Settings → Integrations** in Sequenzy
2. Find **Meta Ads** under Ads & Audiences
3. Click **Connect**, then **Connect Meta account**
4. Sign in with the Meta account that has access to your ad accounts and approve the requested permissions
5. You'll land back in Sequenzy with the connection ready
Your Meta account needs at least one Facebook Page and access to at least one
ad account. Meta requires both for customer list audiences.
Meta also enforces two one-time requirements on the ad account itself before customer list audiences can be created:
1. **The ad account must belong to a Business Portfolio.** Personal ad accounts can't create customer list audiences. Add yours at [Meta Business settings](https://business.facebook.com/settings) under **Accounts → Ad accounts**.
2. **The Custom Audience terms must be accepted** once for the ad account, by an admin.
If either is missing, Sequenzy detects it when you create a sync and shows a direct link to the right Meta page - fix it there, then click try again.
## Creating an audience sync
1. Open **Manage Audiences** on the Meta Ads integration
2. Pick a segment - either one of your existing segments or a ready-made one:
* **Zero LTV** - subscribers who have never paid
* **No purchase in 1 year** - lapsed customers worth re-engaging
* **Recent buyers** - placed an order in the last 30 days
* **High spenders**, **Non-buyers**, **Engaged**, and more
3. Choose the ad account and a name for the audience in Meta
4. Pick a dashboard sync frequency (daily or weekly)
5. Click **Create & sync to Meta**
The dialog previews the active audience size before you create the sync and keeps the segment, ad account, schedule, and Meta audience name visible for a final check.
The first upload starts immediately. After that, the audience refreshes automatically on your schedule. You can also trigger a sync manually, pause a sync, or remove it at any time.
## API, CLI, and MCP
Audience syncs are fully manageable outside the dashboard:
* **API** - [List](/api-reference/audience-syncs/list), [create](/api-reference/audience-syncs/create), [update](/api-reference/audience-syncs/update), [delete](/api-reference/audience-syncs/delete), and [run](/api-reference/audience-syncs/sync) syncs, plus [list ad accounts](/api-reference/audience-syncs/ad-accounts)
* **CLI** - `sequenzy audience-syncs list|create|update|delete|sync|ad-accounts` ([CLI docs](/concepts/cli#audience-syncs))
* **MCP** - `list_audience_syncs`, `create_audience_sync`, `update_audience_sync`, `delete_audience_sync`, `sync_audience_now`, and `list_ad_accounts` ([MCP docs](/concepts/mcp-tools#audience-syncs))
Connecting the Meta account itself happens in the dashboard (it requires a browser OAuth flow with Meta).
## Things to know
* **Audiences are add-only.** Subscribers who leave a segment stay in the Meta audience; new segment members are added on every sync. This is a limitation of the sync, not of Meta - you can remove people manually in Meta Ads Manager, or create a new sync with a new audience name for a clean slate.
* **Meta needs 100+ matched people** before an audience can be used for ad delivery. Small segments will sync but won't be targetable until they grow.
* **Match rates vary.** Meta typically matches 30-70% of uploaded emails to accounts, depending on your audience.
* **Removing a sync keeps the audience.** Deleting a sync in Sequenzy only stops future updates - the audience remains in Meta Ads Manager so running ads aren't disrupted.
* **Unsubscribed and bounced contacts are never uploaded.** Only active subscribers in the segment are synced.
## Troubleshooting
**The sync shows an error**
Open the integration's activity log in Settings → Integrations to see the failure reason. Common causes are a revoked Meta session (reconnect the integration) or an ad account that hasn't accepted Meta's custom audience terms (accept them once in Meta Ads Manager under Audiences).
**The sync shows "Disconnected"**
Meta stopped recognizing the connection - usually after a password change, a revoked permission, or removing the app in Meta. Sequenzy pauses the sync automatically so it doesn't keep failing. Reconnect your Meta account from the audience sync dialog (or Settings → Integrations), then resume the sync - it recreates the audience if needed and picks up on the next run.
**"This ad account needs a Meta Business Portfolio"**
Meta only allows customer list audiences on ad accounts that belong to a business. In [Meta Business settings](https://business.facebook.com/settings), go to **Accounts → Ad accounts**, add the ad account, then create the sync again. You need to be both a business admin and the ad account owner to do this.
**"Meta's terms need a one-time acceptance"**
An admin of the ad account has to accept Meta's Custom Audience terms once. Sequenzy links straight to the acceptance page when this happens - accept there, then create the sync again.
**The audience is smaller than the segment**
This is expected - Meta only counts people it could match to a Facebook or Instagram account by email.
**No ad accounts appear**
Make sure the Meta user you connected with has access to the ad account in Meta Business Manager, then reconnect.
# n8n Integration
Source: https://docs.sequenzy.com/integrations/n8n
Use the Sequenzy community node to trigger n8n workflows from Sequenzy events, add subscribers, manage tags, and send events from n8n workflows.
Use the Sequenzy community node for n8n to trigger workflows from Sequenzy
outbound events, and to send form submissions, billing events, spreadsheet
rows, and other workflow data into Sequenzy.
The package is published as
[`n8n-nodes-sequenzy`](https://www.npmjs.com/package/n8n-nodes-sequenzy). The
source is available on
[GitHub](https://github.com/Sequenzy/n8n-nodes-sequenzy).
## Install the Community Node
Install Sequenzy from n8n's Community Nodes screen:
1. In n8n, go to **Settings → Community Nodes**.
2. Select **Install**.
3. Enter `n8n-nodes-sequenzy`.
4. Confirm the install and restart n8n if your instance asks you to.
5. Add a **Sequenzy** node to a workflow.
For self-hosted deployments, you can also install the package with npm in the
n8n environment:
```bash theme={null}
npm install n8n-nodes-sequenzy
```
Install community nodes only in n8n workspaces where your team allows
community packages. Sequenzy does not require any manual approval outside your
n8n instance.
## Authentication
The n8n node uses Sequenzy API key authentication.
1. In Sequenzy, go to **Settings → API Keys & SMTP**.
2. Create or copy a workspace API key. New workspace keys start with
`seq_live_`; legacy `ek_` keys remain supported.
3. In n8n, create a credential of type **Sequenzy API**.
4. Paste the API key and save the credential.
5. Select that credential on each Sequenzy node.
By default, the credential sends requests to
`https://api.sequenzy.com/api/v1` with:
```txt theme={null}
Authorization: Bearer YOUR_API_KEY
```
## Supported Operations
The package includes a trigger node and an action node.
| Node | Resource/Operation | What it does |
| - | - | - |
| Sequenzy Trigger | Events | Starts a workflow from selected Sequenzy outbound webhook events |
| Sequenzy | Subscriber/Create or Update | Creates or updates a subscriber by email or external ID |
| Sequenzy | Subscriber/Get | Looks up a subscriber by email or external ID |
| Sequenzy | Subscriber/Delete | Deletes a subscriber by email or external ID |
| Sequenzy | Tag/Add | Adds a tag to a subscriber, creating the subscriber if needed |
| Sequenzy | Tag/Remove | Removes a tag from a subscriber |
| Sequenzy | Event/Trigger | Triggers a custom event for a subscriber, creating the subscriber if needed |
### Trigger Events
Use **Sequenzy Trigger** when Sequenzy should start an n8n workflow. Choose one
or more outbound events; the default is **New Subscriber**
(`subscriber.created`). The trigger creates an ordinary Sequenzy outbound
webhook endpoint using n8n's production webhook URL, stores the one-time HMAC
signing secret in the workflow's node static data, and verifies
`X-Sequenzy-Signature` before starting the workflow.
Common trigger events:
* `subscriber.created` for new subscribers
* `campaign.sent` when an email or SMS campaign reaches its final sent state
* `email.replied` for inbound replies
* `subscriber.unsubscribed` for opt-outs
* `sequence.finished` and `sequence.failed` for lifecycle automation
The trigger outputs the full Sequenzy event envelope, including `id`, `type`,
`created_at`, `metric`, and `data`.
Delivery uses the same durable outbound-webhook pipeline as every other
Sequenzy endpoint: each request is HMAC-signed and failed deliveries are tried
up to 16 times across seven days. Per-message events such as `email.opened`,
`email.clicked`, and `email.sent` can start one n8n execution per recipient, so
select them only when the workflow is sized for that volume.
### Subscriber Fields
Create or Update supports:
* Email or External ID
* First name
* Last name
* Tags as a comma-separated list
* Custom attributes as key-value pairs
* Enroll in Sequences
Get and Delete accept either Email or External ID.
### Tag Fields
Add and Remove require a tag plus either Email or External ID. Add also accepts
first and last name values for cases where Sequenzy needs to create the
subscriber before applying the tag.
### Event Fields
Trigger requires an event name plus either Email or External ID. You can also
send first name, last name, and event properties as key-value pairs.
Use event names that match your Sequenzy automations, such as
`signup.completed`, `trial_started`, `checkout.completed`, or
`subscription.canceled`.
## Example Workflows
### Webhook or Form to Sequenzy Subscriber
Use this when a custom form, landing page, or internal app can POST to n8n.
```txt theme={null}
Webhook/Form Trigger -> Set -> Sequenzy: Subscriber Create or Update
```
Map the incoming email to the Sequenzy Email field. Put form answers you want to
segment on into Custom Attributes, and add tags such as `lead`, `demo-request`,
or `newsletter`.
### Typeform, Tally, or Jotform to Sequenzy Subscriber
Use the native app node when your n8n instance has one, or use the app's webhook
feature with n8n's Webhook node.
```txt theme={null}
Typeform/Tally/Jotform -> Set -> Sequenzy: Subscriber Create or Update
```
Recommended mapping:
| Form value | Sequenzy field |
| - | - |
| Email question | Email |
| Name question | First Name or Last Name |
| Hidden user/customer ID | External ID |
| Plan, company size, source | Custom Attributes |
| Form name or intent | Tags |
### Stripe Event to Sequenzy Event
Use this when payment activity should trigger Sequenzy automations.
```txt theme={null}
Stripe Trigger/Webhook -> Set -> Sequenzy: Event Trigger
```
Recommended events:
* `stripe.checkout_completed`
* `stripe.subscription_created`
* `stripe.payment_failed`
* `stripe.subscription_canceled`
Map the customer email or your own customer ID, then pass Stripe values such as
price ID, product name, subscription status, amount, currency, or trial end as
event properties.
### Google Sheets Row to Sequenzy Subscriber
Use this when a spreadsheet is the source of leads or imported contacts.
```txt theme={null}
Google Sheets Trigger/Read Rows -> Sequenzy: Subscriber Create or Update
```
Map the email column to Email. Map name columns to First Name and Last Name. Map
other columns such as company, role, plan, or source to Custom Attributes.
### Sequenzy Event to n8n Workflow
Use this when Sequenzy should notify n8n about email, campaign, subscriber, or
sequence lifecycle events.
```txt theme={null}
Sequenzy Trigger -> Switch/Set -> Any n8n action
```
Add a **Sequenzy Trigger** node, select your **Sequenzy API** credential, and
choose the events your workflow needs. n8n must provide a production HTTPS
webhook URL when the workflow is activated. The API key must have account read
and webhook read, write, and delete permissions (`account:read`,
`webhooks:read`, `webhooks:write`, and `webhooks:delete`). Account read lets n8n
validate the shared Sequenzy credential; the webhook permissions let the node
create, reconcile, and clean up its managed outbound webhook endpoint.
### Manual Fallback: Sequenzy Outbound Webhook to n8n Webhook Node
Use this if you prefer n8n's generic Webhook node or cannot install community
trigger nodes.
```txt theme={null}
Sequenzy Outbound Webhook -> n8n Webhook -> Switch/Set -> Any n8n action
```
Create an n8n **Webhook** node first and copy its production URL. Then create a
Sequenzy outbound webhook endpoint pointing to that URL. Subscribe to the
Sequenzy events your workflow needs, such as `campaign.sent`, `email.replied`,
`subscriber.unsubscribed`, `sequence.finished`, or `email.clicked`.
See [Outbound Webhooks](/integrations/outbound-webhooks) for payloads,
signatures, retries, and delivery behavior.
## Limitations
* The Sequenzy Trigger manages one Sequenzy outbound webhook endpoint per
active workflow. Deactivating the workflow deletes that managed endpoint.
* The node uses API key authentication. OAuth is not supported.
* The node runs once per incoming n8n item. For high-volume imports, prefer the
Sequenzy bulk API endpoints or batch your workflow carefully.
* Custom attributes and event properties are configured as key-value pairs in
the node UI. Use a Set or Code node before Sequenzy if you need to normalize
complex source data.
* Subscriber lookup returns data only when a matching subscriber is found. Build
your workflow so the not-found path is handled explicitly.
## Troubleshooting
### Test Against a Local Sequenzy API
The node uses `https://api.sequenzy.com/api/v1` by default. When developing the
node locally, start n8n with `SEQUENZY_API_BASE_URL` set to the local API:
```bash theme={null}
SEQUENZY_API_BASE_URL=http://127.0.0.1:4000/api/v1 npm run dev
```
Restart n8n after changing the value. If the variable is unset or blank, the
node uses the production Sequenzy API. This override is for local or self-hosted
n8n because n8n Cloud does not expose custom environment variables to community
nodes.
To test the trigger locally, expose n8n's port with ngrok:
```bash theme={null}
brew install ngrok
ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN
ngrok http 5678
```
Keep ngrok running, copy its HTTPS forwarding URL, and restart n8n:
```bash theme={null}
SEQUENZY_API_BASE_URL=http://127.0.0.1:4000/api/v1 \
N8N_WEBHOOK_URL=https://YOUR-NGROK-DOMAIN.ngrok.app/ \
N8N_PROXY_HOPS=1 \
npm run dev
```
### Credential Test Fails
Confirm the workspace API key starts with `seq_live_` (or the legacy `ek_`
prefix), has not been deleted, and was pasted into the **Sequenzy API**
credential without extra spaces.
### Subscriber Was Not Created
Confirm the workflow provides either Email or External ID. For new subscribers,
Email is recommended because Sequenzy needs an address before it can send email.
### Automation Did Not Start
Confirm the Sequenzy automation is published and listens for the exact event
name or tag used by the n8n node. Event names and tag names are case-sensitive.
### Outbound Webhook Did Not Reach n8n
For **Sequenzy Trigger**, confirm the workflow is active, n8n exposes a public
HTTPS production URL, and the credential has account read plus webhook read,
write, and delete permissions. Deactivate and reactivate the workflow to
recreate an endpoint whose one-time signing secret is missing. Then check
webhook delivery status in Sequenzy and execution history in n8n.
For the manual fallback, use the production URL from the n8n Webhook node, not
its temporary test URL.
## Resources
* [n8n package on npm](https://www.npmjs.com/package/n8n-nodes-sequenzy)
* [Sequenzy n8n node on GitHub](https://github.com/Sequenzy/n8n-nodes-sequenzy)
* [n8n Community Nodes documentation](https://docs.n8n.io/integrations/community-nodes/installation/)
* [Sequenzy API Reference](/api-reference/introduction)
* [Outbound Webhooks](/integrations/outbound-webhooks)
# Outbound Webhooks
Source: https://docs.sequenzy.com/integrations/outbound-webhooks
Send campaign, email, SMS, subscriber, and sequence lifecycle events from Sequenzy to your own endpoint.
You can send email lifecycle events to your own HTTPS endpoint when messages are sent, delivered, delayed, bounced, complained, opened, clicked, replied to, or unsubscribed. You can also receive aggregate email and SMS campaign completion, SMS lifecycle, subscriber profile and per-list consent, and sequence lifecycle events.
## Events
Supported event types:
```json theme={null}
[
"email.sent",
"email.delivered",
"email.delivery_delayed",
"email.bounced",
"email.failed",
"email.complained",
"email.opened",
"email.clicked",
"email.replied",
"email.unsubscribed",
"campaign.sent",
"sms.sent",
"sms.delivered",
"sms.failed",
"sms.opted_out",
"subscriber.invalid",
"subscriber.updated",
"subscriber.created",
"subscriber.unsubscribed",
"subscriber.list_subscribed",
"subscriber.list_unsubscribed",
"subscriber_import.completed",
"sequence.finished",
"sequence.failed",
"poll.answered"
]
```
`email.bounced` and `email.failed` mean different things and you should handle them differently. `email.bounced` is the receiving provider telling us the mailbox is bad, and the address is suppressed. `email.failed` means every delivery route for that message was exhausted without the provider ever judging the mailbox - the address stays subscribed and will be tried again on your next send, so do not suppress on it. Its payload carries a `failure` object with a `code` of either `transport_exhausted` or `admin_bounce`.
New webhook endpoints subscribe to sent, delivered, delayed, bounced, failed, complained, email unsubscribed, SMS lifecycle (`sms.*`), invalid subscriber, subscriber created, and subscriber unsubscribed events by default. Add `campaign.sent`, `email.opened`, `email.clicked`, `email.replied`, `subscriber.updated`, `subscriber.list_subscribed`, `subscriber.list_unsubscribed`, `subscriber_import.completed`, `sequence.finished`, `sequence.failed`, and `poll.answered` explicitly if you want aggregate campaign completion, high-volume engagement, inbound reply, profile sync, per-list consent sync, import completion, sequence lifecycle, or poll answer events. `campaign.sent` is opt-in. `poll.answered` payloads include a `poll` object with the block id, question, answer, stored value, and attribute key.
## Payload
```json theme={null}
{
"id": "evt_123",
"type": "email.delivered",
"object": "event",
"metric": "delivered",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"email_send_id": "send_123",
"message_id": "ses-message-id",
"campaign_id": "camp_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"recipient": "user@example.com",
"subject": "Welcome",
"email_type": "campaign",
"metadata": {
"source": "campaign"
}
}
}
```
Use `id` to deduplicate events. Webhook payloads use one canonical snake\_case field per value. `external_id` is included when the recipient is linked to a subscriber with a customer-owned external ID, or when a single-recipient transactional send included `subscriberExternalId` even if no subscriber exists.
`campaign.sent` fires once when an email or SMS campaign reaches its terminal
`sent` state, including sends with no recipients and completed email A/B tests.
It is one aggregate event per campaign delivery generation, not one event per
recipient. Retries and resumed finalization use the same deterministic event ID
for that generation.
```json theme={null}
{
"id": "evt_campaign_123",
"type": "campaign.sent",
"object": "event",
"metric": "sent",
"created_at": "2026-08-25T12:00:00.000Z",
"data": {
"campaign_id": "camp_123",
"campaign_name": "August launch",
"campaign_type": "email",
"status": "sent",
"sent_at": "2026-08-25T12:00:00.000Z",
"total_sent": 2400,
"total_failed": 3
}
}
```
`campaign_type` is `email`, `sms`, or `push`. For push campaigns, `total_sent` counts contacts whose notification at least one push service accepted and `total_failed` counts contacts whose every device rejected it.
SMS events fire when an SMS sequence step is accepted by the carrier network (`sms.sent`), confirmed delivered (`sms.delivered`), rejected or undeliverable (`sms.failed`), or when a recipient replies STOP (`sms.opted_out`). Test sends do not emit webhooks. An opt-out applies to every subscriber sharing the phone number; `opted_out_subscriber_ids` lists all of them.
```json theme={null}
{
"id": "evt_sms",
"type": "sms.delivered",
"object": "event",
"metric": "delivered",
"created_at": "2026-07-07T12:00:00.000Z",
"data": {
"sms_send_id": "sms_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"automation_id": "seq_123",
"automation_node_id": "node_123",
"phone": "+15550100123",
"from_number": "+18885550123",
"segments": 1,
"credits_charged": 1,
"provider_message_id": "provider-message-id"
}
}
```
Inbound reply events include the stored reply, conversation, original send context, body text, HTML body, and stripped text. Attachment bodies are not included; only metadata is delivered.
```json theme={null}
{
"id": "evt_reply",
"type": "email.replied",
"object": "event",
"metric": "replied",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"reply_id": "reply_123",
"conversation_id": "conv_123",
"conversation_message_id": "msg_123",
"email_send_id": "send_123",
"message_id": "reply-message-id",
"in_reply_to": "ses-message-id",
"campaign_id": "camp_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"from_email": "user@example.com",
"from_name": "Ada",
"recipient": "user@example.com",
"subject": "Re: Welcome",
"email_type": "campaign",
"body_text": "Thanks for the update.",
"body_html": "
Thanks for the update.
",
"stripped_text": "Thanks for the update.",
"has_attachments": false,
"attachment_count": 0,
"received_at": "2026-05-05T12:00:00.000Z"
}
}
```
Subscriber events include the current subscriber profile when one exists. `subscriber.created` fires once per newly created contact and carries `source` (the origin, using the same vocabulary as the per-list consent events) plus `list_ids` for the lists it joined at creation. It fires for every creation path, including bulk imports and integration syncs, so filter on `source` if you only want organic signups. When double opt-in is on, it fires at confirmation rather than at signup, with `source` set to `double_opt_in` - a contact who never confirms never emits it. `subscriber.invalid` fires when a subscriber add attempt cannot create a sendable subscriber because the attempted email is missing, syntactically invalid, has an invalid/blocked domain, or is already suppressed from previous delivery failures. `subscriber.updated` fires when `email`, `external_id`, `first_name`, `last_name`, `timezone`, `custom_attributes`, or a non-unsubscribe `status` change occurs. Active to unsubscribed changes emit `subscriber.unsubscribed`; they only also emit `subscriber.updated` when another profile field changes in the same update. List and tag changes do not emit `subscriber.updated`.
Use `subscriber.list_subscribed` and `subscriber.list_unsubscribed` for per-list consent sync. They fire only when list membership actually changes, so retrying an already-applied add or remove does not create another event. The payload identifies the subscriber and list, reports `action` as `added` or `removed`, and includes a stable `source` such as `preferences_page`, `dashboard`, `api`, `automation`, `import`, `double_opt_in`, or an integration name such as `stripe`. A global opt-out continues to emit `subscriber.unsubscribed` without fanning out one event per list.
```json theme={null}
{
"id": "evt_list_123",
"type": "subscriber.list_unsubscribed",
"object": "event",
"metric": "list_unsubscribed",
"created_at": "2026-07-14T12:00:00.000Z",
"data": {
"subscriber_id": "sub_123",
"external_id": "customer_123",
"email": "user@example.com",
"list_id": "list_product_updates",
"list_name": "Product updates",
"action": "removed",
"source": "preferences_page"
}
}
```
```json theme={null}
{
"id": "evt_invalid",
"type": "subscriber.invalid",
"object": "event",
"metric": "invalid",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"email": "bad@gmai.com",
"external_id": "customer_123",
"first_name": "Ada",
"custom_attributes": {
"plan": "pro"
},
"reason_code": "invalid_email_domain",
"reason": "Invalid domain for \"bad@gmai.com\": Domain is blacklisted (typosquatting or disposable email)",
"metadata": {
"source": "add_subscriber"
}
}
}
```
```json theme={null}
{
"id": "evt_789",
"type": "subscriber.updated",
"object": "event",
"metric": "updated",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"subscriber_id": "sub_123",
"external_id": "customer_new",
"email": "new@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"timezone": "America/New_York",
"status": "active",
"custom_attributes": {
"plan": "pro"
},
"changed_fields": ["email", "external_id", "timezone", "custom_attributes"],
"previous": {
"email": "old@example.com",
"external_id": "customer_old",
"timezone": "America/Chicago",
"custom_attributes": {
"plan": "starter"
}
}
}
}
```
Sequence lifecycle payloads include the subscriber email, external ID when available, and the underlying sequence event data:
```json theme={null}
{
"id": "evt_456",
"type": "sequence.failed",
"object": "event",
"metric": "failed",
"created_at": "2026-05-05T12:00:00.000Z",
"data": {
"sequence_id": "seq_welcome",
"sequence_name": "Welcome sequence",
"automation_id": "seq_welcome",
"automation_name": "Welcome sequence",
"automation_token_id": "token_123",
"token_id": "token_123",
"subscriber_id": "sub_123",
"external_id": "customer_123",
"email": "user@example.com",
"lifecycle": "failed",
"status": "failed",
"current_node_id": "node_email_2",
"reason": "Email template email_123 not found",
"event": {
"id": "event_123",
"name": "sequence_failed",
"properties": {
"sequence_id": "seq_welcome",
"sequence_name": "Welcome sequence",
"automation_id": "seq_welcome",
"automation_name": "Welcome sequence",
"automation_token_id": "token_123",
"token_id": "token_123",
"lifecycle": "failed",
"status": "failed",
"current_node_id": "node_email_2",
"reason": "Email template email_123 not found"
}
}
}
}
```
`subscriber_import.completed` has one stable logical event ID when a bulk subscriber import reaches its terminal state, carrying the final reconciled counts - `added_count + updated_count + skipped_count + failed_count` equals `total_rows`, with per-reason breakdowns in `skipped_reasons` and `failed_reasons`. Transient enqueue failures are repaired automatically. HTTP delivery can retry, so deduplicate by the top-level event `id` as with every webhook:
```json theme={null}
{
"id": "evt_import_123",
"type": "subscriber_import.completed",
"object": "event",
"metric": "completed",
"created_at": "2026-08-25T12:00:00.000Z",
"data": {
"import_id": "import_abc123",
"batch_id": "batch_abc123",
"file_name": "warehouse-sync.csv",
"status": "completed",
"total_rows": 5000,
"processed_count": 5000,
"added_count": 4200,
"updated_count": 700,
"skipped_count": 80,
"failed_count": 20,
"duplicate_rows": 60,
"total_chunks": 50,
"completed_chunks": 50,
"failed_chunks": 0,
"skipped_reasons": { "duplicate_csv_row": 60, "already_unsubscribed": 20 },
"failed_reasons": { "invalid_email_domain": 20 },
"completed_at": "2026-08-25T12:00:00.000Z"
}
}
```
## Signatures
Every request includes:
```txt theme={null}
X-Sequenzy-Event-Id: evt_123
X-Sequenzy-Event-Type: email.delivered
X-Sequenzy-Timestamp: 1777977600
X-Sequenzy-Signature: v1=...
```
To verify a request, compute an HMAC-SHA256 digest over:
```txt theme={null}
v1:{timestamp}:{raw_request_body}
```
Use an active webhook signing secret returned when you create the webhook or add a signing secret. Compare the result to any `v1=` value in `X-Sequenzy-Signature`.
If a webhook has multiple active signing secrets, Sequenzy still sends one POST request and includes one signature per secret in the same header:
```txt theme={null}
X-Sequenzy-Signature: v1=abc123,v1=def456
```
To change secrets without downtime, add a new secret, deploy it in your receiver, then remove the old secret.
## Delivery
Your endpoint must use HTTPS and return a `2xx` status within 4 seconds. Failed requests receive up to 16 delivery attempts with exponential backoff spanning 7 days. Delivery rows keep the latest status, error, and failed-response snippet. You can replay a delivery manually from the API.
Sequenzy connects only to public HTTPS targets. Delivery workers resolve and validate the hostname before each attempt, then connect to that vetted address while preserving the original host for TLS and request verification.
If an endpoint repeatedly fails, delivery pauses for 10 minutes for that endpoint so other webhook endpoints keep moving. After the pause, Sequenzy sends one pending delivery to test the endpoint and releases the rest only once it succeeds. Successful delivery resets the endpoint failure counter. Editing the endpoint URL, re-enabling the endpoint, sending a test event, or replaying a delivery also resets the stored failure state so you can verify a fixed receiver immediately.
If every delivery to an endpoint fails for 7 days, Sequenzy disables the endpoint, skips its pending deliveries, and emails the workspace owner. The endpoint shows as disabled automatically in Webhooks, and the API returns the time in `autoDisabledAt`. Once your receiver returns `2xx` again, enable the endpoint to receive new events.
Create and manage endpoints with the [webhooks API](../api-reference/webhooks/outbound).
# Paddle Integration
Source: https://docs.sequenzy.com/integrations/paddle
Sync revenue data from Paddle to power segments and automations
Connect your Paddle account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## What is Paddle?
[Paddle](https://paddle.com) is a complete payments infrastructure for SaaS companies. It handles payments, subscriptions, tax compliance, and more as a Merchant of Record, so you don't have to worry about sales tax, VAT, or payment processing complexities.
## Connecting Paddle
Paddle requires manual API key and webhook setup. Here's how to connect:
### Step 1: Get Your Paddle API Key
1. Go to your [Paddle Dashboard](https://vendors.paddle.com)
2. Navigate to **Developer Tools → Authentication**
3. Create a new **API Key**
4. Copy the API key (format: `pdl_live_xxxxxxxxx` or `pdl_test_xxxxxxxxx`)
Your API key is only shown once. Store it securely before closing the dialog.
### Step 2: Find Your Seller ID
1. In the Paddle Dashboard, go to **Settings**
2. Your **Seller ID** is displayed at the top (a numeric ID like `123456`)
3. Copy this ID for later
### Step 3: Set Up Webhooks in Paddle
1. In Paddle, go to **Developer Tools → Notifications**
2. Click **Create destination**
3. Select **Webhook** as the destination type
4. Enter your Sequenzy webhook URL:
```text theme={null}
https://api.sequenzy.com/api/webhooks/paddle/{your-company-id}
```
You can find this URL in Sequenzy when connecting Paddle. It's
automatically shown in the connection modal.
5. Select these events to listen for:
* `subscription.created`
* `subscription.updated`
* `subscription.canceled`
* `subscription.paused`
* `subscription.resumed`
* `subscription.activated`
* `subscription.trialing`
* `subscription.past_due`
* `transaction.completed`
* `transaction.payment_failed`
* `adjustment.created`
6. Click **Save** to create the notification destination
7. Copy the **Secret Key** from the notification destination settings
### Step 4: Connect in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Paddle
3. Enter your:
* **Seller ID** (from Step 2)
* **API Key** (from Step 1)
* **Webhook Secret Key** (from Step 3)
4. Click **Connect Paddle**
Sequenzy will immediately begin receiving webhook events from Paddle.
Existing destinations using `/webhooks/paddle/{your-company-id}` remain supported.
Sequenzy verifies each notification's signature and returns HTTP 200 once the
notification is queued. Subscriber updates and `saas.*` events run in the background.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` subscriptions count toward MRR.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Paddle subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active subscription |
| `cancelled` | Subscription set to cancel at period end |
| `churned` | Subscription has ended |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes via webhooks.
## Tracked Events
When you connect Paddle, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | New subscription created or transaction completed |
| `saas.purchase.monthly` | Monthly subscription purchase |
| `saas.purchase.yearly` | Yearly subscription purchase |
| `saas.trial_started` | Subscription enters trial period |
| `saas.cancelled` | Subscription set to cancel |
| `saas.churn` | Subscription canceled or paused |
| `saas.payment_failed` | Transaction payment failed or subscription past due |
| `saas.refund` | Adjustment (refund/chargeback) created |
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100`: High MRR customers
* `ltv > 1000`: Lifetime high-value customers
* `mrr >= 50 AND mrr < 200`: Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500`: High-value churned customers (win-back targets)
* `tag = cancelled AND mrr >= 100`: At-risk high-value customers needing attention
* `tag = saas.yearly AND mrr >= 100`: Yearly enterprise customers
### Automations
Trigger automations when revenue attributes change:
* Send a thank-you email when `ltv` exceeds a threshold
* Trigger a win-back sequence when a subscriber gets the `churned` tag
* Notify your team when a high-value customer cancels
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Paddle customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If the webhook only contains a customer ID, Sequenzy uses your Paddle API key to
look up the email during background processing. Supported events can create a
subscriber when no existing subscriber matches. If no email can be found, no
subscriber event is emitted. Temporary Paddle API failures are retried in the
background.
## Troubleshooting
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Paddle notification settings
2. Check that you've selected the correct events
3. Check Paddle's notification logs for failed delivery attempts
4. Ensure your Sequenzy webhook endpoint is accessible
If delivery history shows HTTP 404 with `NOT_FOUND`, check that the destination
uses `https://api.sequenzy.com/api/webhooks/paddle/{your-company-id}` (the older
`/webhooks/paddle/{your-company-id}` URL also works). Reconnecting alone does not
replay failed notifications. Once delivery works again, you can
[replay failed notifications in Paddle](https://developer.paddle.com/api-reference/notifications/replay-notification/).
Replays retain the original event ID so repeated deliveries do not duplicate
the same `saas.*` event.
### Invalid Webhook Signature
If webhooks are failing with signature errors:
1. Verify you copied the complete webhook secret from Paddle
2. Re-create the notification destination in Paddle and update the secret in Sequenzy
3. Ensure you're using the correct secret (each destination has its own secret)
### Customer Email Not Found
Paddle webhooks may include only a customer ID. If events are being acknowledged but subscribers aren't being updated:
1. Ensure the customer email in Paddle matches a subscriber in Sequenzy
2. Check that customers have valid email addresses in Paddle
3. Ensure your Paddle API key can read customers in the same sandbox or live account as the notification destination
# Polar Integration
Source: https://docs.sequenzy.com/integrations/polar
Sync revenue data from Polar to power segments and automations
Connect your Polar account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## What is Polar?
[Polar](https://polar.sh) is a modern payment platform designed for developers, open source maintainers, and digital product creators. It offers subscriptions, one-time payments, and benefits management with a developer-friendly API.
## Connecting Polar
Polar connects with an API token and a webhook you create in Polar.
### Step 1: Get Your Polar API Key
1. Go to your [Polar Dashboard](https://polar.sh)
2. Navigate to **Settings** in your organization
3. Scroll to the bottom and create a new token with these scopes:
`organizations:read`, `customers:read`, `subscriptions:read`, and
`orders:read`
4. Set the expiration to **No Expiration**
5. Copy the token (format: `polar_oat_xxxxxxxxx`)
Your API token is only shown once. Store it securely before closing the
dialog.
### Step 2: Set Up Webhooks in Polar
1. In Polar, go to **Settings → Webhooks**
2. Click **Create Webhook Endpoint**
3. Enter your Sequenzy webhook URL:
```
https://api.sequenzy.com/api/webhooks/polar/{your-company-id}
```
You can find this URL in Sequenzy when connecting Polar. It's automatically
shown in the connection modal.
4. Select these events to listen for:
* `subscription.created`
* `subscription.active`
* `subscription.updated`
* `subscription.canceled`
* `subscription.uncanceled`
* `subscription.revoked`
* `order.created`
* `order.paid`
* `order.refunded`
* `checkout.expired`
* `customer.created`
* `customer.state_changed`
5. Click **Create** to save the webhook
6. Copy the **Webhook Secret** (format: `polar_whs_xxxxxxxxx`)
Already connected? Polar only delivers the events selected on your existing
webhook endpoint. Edit it in **Polar → Settings → Webhooks** and make sure
`checkout.expired` and `subscription.uncanceled` are checked - otherwise
abandoned-checkout tracking and trial-reactivation handling will not receive
their events.
### Step 3: Connect in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Polar
3. Enter your:
* **API Token** (from Step 1)
* **Webhook Secret** (from Step 2)
4. Click **Connect Polar**
Sequenzy will immediately begin syncing your existing Polar customers.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` subscriptions count toward MRR.
### LTV Calculation
LTV sums all successful orders for a customer, minus any refunds. This gives you the true lifetime value accounting for partial refunds.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Polar subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active subscription |
| `cancelled` | Subscription set to cancel at period end |
| `churned` | Subscription has ended (revoked) |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes - both from live webhooks and from Sync Revenue runs, which apply the same rules.
## Tracked Events
When you connect Polar, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | New paid subscription or renewal (trials are not purchases) |
| `saas.purchase.monthly` | Monthly subscription purchase |
| `saas.purchase.yearly` | Yearly subscription purchase |
| `saas.trial_started` | Subscription created in trial |
| `saas.trial_will_end` | 3 days before a trial ends (scheduled by Sequenzy) |
| `saas.trial_ended` | Trial converted to a paid subscription (first paid renewal) |
| `saas.trial_cancelled` | Trial cancelled before converting |
| `saas.cancelled` | Subscription set to cancel at period end |
| `saas.churn` | Subscription revoked/ended |
| `saas.upgrade` | Subscription upgraded to higher plan |
| `saas.downgrade` | Subscription downgraded to lower plan |
| `saas.refund` | Order refunded |
| `saas.checkout_expired` | Checkout abandoned without completing |
Polar does not send a trial-ending webhook, so Sequenzy schedules
`saas.trial_will_end` itself when a trial starts - about 3 days before the
trial's end, matching Stripe's timing. The reminder is cancelled automatically
if the trial converts, is cancelled, or churns first, so automations built on
this event never reach someone who already decided.
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100`: High MRR customers
* `ltv > 1000`: Lifetime high-value customers
* `mrr >= 50 AND mrr < 200`: Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500`: High-value churned customers (win-back targets)
* `tag = cancelled AND mrr >= 100`: At-risk high-value customers needing attention
* `tag = saas.yearly AND mrr >= 100`: Yearly enterprise customers
### Automations
Trigger automations when revenue attributes change:
* Send a thank-you email when `ltv` exceeds a threshold
* Trigger a win-back sequence when a subscriber gets the `churned` tag
* Notify your team when a high-value customer cancels
## Manual Sync
To re-sync revenue data at any time:
1. Go to **Settings → Integrations**
2. Find your connected Polar account
3. Click **Sync Revenue**
The sync processes all Polar customers and updates matching subscribers.
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Polar customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If a Polar customer doesn't have an email or doesn't match any subscriber, they're skipped during sync.
## Troubleshooting
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Polar settings
2. Check that you've selected the correct events
3. Check that the webhook endpoint is enabled in Polar
4. Check Polar's webhook delivery logs for failed attempts
### Customers Not Syncing
1. Ensure the customer email in Polar matches a subscriber in Sequenzy
2. Try clicking **Sync Revenue** to force a re-sync
3. Check that your API token has the required scopes:
* `organizations:read`
* `customers:read`
* `subscriptions:read`
* `orders:read`
### Invalid Webhook Signature
If webhooks are failing with signature errors:
1. Verify you copied the complete webhook secret from Polar
2. Re-create the webhook in Polar and update the secret in Sequenzy
3. Ensure the secret starts with `polar_whs_`
# PostHog Integration
Source: https://docs.sequenzy.com/integrations/posthog
Trigger email automations based on product analytics events
Connect your PostHog account to trigger email automations based on product analytics events. Send emails when users perform (or don't perform) specific actions in your product.
## What is PostHog?
[PostHog](https://posthog.com) is an open-source product analytics platform that tracks user behavior, feature usage, and more. It's popular with SaaS teams who want self-hosted analytics.
## Prerequisites
Before setting up the integration, ensure your PostHog setup includes user emails:
```javascript theme={null}
// When a user logs in, identify them with their email
posthog.identify(userId, {
email: "user@example.com",
name: "John Doe",
});
```
Events from anonymous users (without email) are automatically skipped. Only
events from identified users with an email address will trigger automations in
Sequenzy.
## Connecting PostHog
### Step 1: Get Your Webhook Configuration
1. Go to **Settings → Integrations** in Sequenzy
2. Find **PostHog** under Product Analytics
3. Click **Connect**
4. Copy the **Webhook URL** and **Webhook Secret** shown
### Step 2: Create Webhook Destination in PostHog
1. Go to your [PostHog Dashboard](https://app.posthog.com)
2. Navigate to **Data Pipelines** → **Destinations**
3. Click **+ New** → **Destination**
4. Search for **"Webhook"** in the list and click **+ Create**
5. Configure the webhook:
* **Webhook URL**: Paste your Sequenzy webhook URL
* **Headers**: Add `X-Webhook-Secret` with your secret from Step 1
6. Optionally filter which events to send (or send all - we'll handle filtering)
7. Click **Create & Enable**
### Step 3: Complete Connection in Sequenzy
1. Return to Sequenzy's PostHog connection modal
2. Optionally keep **Import event history** enabled and paste a personal API key (see below) - Sequenzy detects your region and project from the key
3. Click **Connect PostHog**
That's it! PostHog events will now flow into Sequenzy.
## Importing Event History
The webhook only delivers events from the moment you connect. If you want your
existing PostHog data too, enable **Import event history** while connecting and
provide a single input:
* **Personal API key**: create one in PostHog under **Settings → Personal API
keys** with both the **Query Read** and **Project Read** scopes. The key is
encrypted at rest and never shown again.
Sequenzy verifies the key against PostHog Cloud US and EU, detects your region
automatically, and lists the projects the key can access. If the key reaches a
single project it is selected for you; if it reaches several you pick one from a
list. You never need to look up your region or numeric project ID by hand.
If PostHog cannot be reached while the key is being verified, the modal falls
back to letting you enter your region and project ID manually so you can still
connect.
A key with only **Query Read** still verifies and detects your region, but
Sequenzy cannot list your projects - you will be asked to enter the numeric
project ID, found in PostHog under **Settings → Project**. Add the **Project
Read** scope to have the project selected for you. An invalid key is rejected
the moment you verify it, instead of surfacing later as a failed background
import.
Sequenzy then imports up to 5 years of identified events in the background.
Imported events keep their original timestamps, so an event-based segment such
as "performed `product.signed_up` in the last 30 days" is accurate immediately
after connecting.
### What historical events do and do not do
Historical events count everywhere you look at data: event-based segments,
campaign audiences, filters, and the contact timeline. They deliberately do
**not** run anything: no sequences are started, no waiting steps resume, no
sync rules apply, and no goal conversions are recorded. Importing years of
history can never send an email.
A few more rules:
* Only identified events with a resolvable email are imported; anonymous rows
are counted as skipped
* Rows whose address cannot be used - malformed, a dead or disposable domain,
or one that has already hard-bounced - are counted as skipped too. A handful
of unusable addresses in an old archive never fails the import
* Your event allowlist applies to history too. Without an allowlist, PostHog
internal events (names starting with `$`, like `$pageview` and
`$autocapture`) are excluded from the import to avoid flooding your workspace
with pageview noise - add them to the allowlist explicitly if you want them
* Historical `$identify` events never modify subscriber profiles; only live
ones do
* Re-running the import is safe: events are deduplicated by PostHog's own event
ID, including against events that already arrived through the webhook
### Import status and retry
The PostHog card in **Settings → Integrations** shows the import state:
importing, the number of events imported, or a failure message with a **Retry
import** button. A failed history import never affects the real-time webhook
connection. The same status is visible from the CLI (`sequenzy integrations
list`) and the MCP `list_integrations` tool.
Retry is available from every surface, and they all do the same thing:
```bash theme={null}
sequenzy integrations sync int_posthog
```
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/integrations/int_posthog/sync" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The retry restarts the import from the beginning rather than resuming, which is
safe because events are deduplicated by PostHog's own event ID - a re-run writes
only what is missing. A retry is refused while an import is still reporting
progress. If BullMQ exhausts retries or declares the job terminally stalled, the
integration is marked failed and can be retried.
## What Gets Synced
Custom events are accepted by default. Connections created from August 2026 on
also skip PostHog-internal events (names starting with `$`, like `$pageview`
and `$autocapture`) unless you allowlist them, because that traffic usually
swamps the events you actually automate on - `$identify` always passes so
profile updates keep working. Integrations connected earlier keep receiving
everything their webhook destination forwards; you can enable the same
filtering from the integration's event settings. When an event is received,
Sequenzy:
1. Checks for an email in the event properties (`email`, `user_email`, `$email`, or nested `user.email`), identify `$set` properties, or `person.properties`
2. **Skips events without email** - anonymous events are silently ignored
3. Creates or updates the subscriber with that email
4. Records the event with the `product.` prefix (e.g., `product.$pageview`, `product.feature_used`)
5. Triggers any automations listening for that event
### Which lists new contacts join
PostHog has no per-integration list targeting. An identified person who is not
already a subscriber is created and joined to your workspace
[Default Lists](/concepts/subscribers#default-lists), the same setting forms,
imports, and Dodo Payments follow.
A JSON `null` there means **every current and future list**, not "no lists".
`[]` joins nothing. Read and set it before you rely on list-scoped segments:
```bash theme={null}
sequenzy companies get comp_abc123
sequenzy companies update comp_abc123 --default-list list_product_users
```
Over MCP the field is `defaultSubscriberListIds` on `get_company` /
`update_company`. Changing it only affects later writes and repoints every
other untargeted contact source too. This setting applies to live PostHog
events only. Historical imports create new contacts without any list
memberships and do not enroll sequences.
### Event Naming
PostHog events are prefixed with `product.` in Sequenzy:
| PostHog Event | Sequenzy Event |
| - | - |
| `feature_used` | `product.feature_used` |
| `onboarding_completed` | `product.onboarding_completed` |
| `$pageview` | `product.$pageview` |
### Synced Attributes
Only `$identify` events update subscriber attributes. Raw properties from all other events stay on the event (available to automations and event-based segments) and are never copied onto the subscriber profile:
| Attribute | Source |
| - | - |
| `posthogDistinctId` | PostHog distinct\_id |
| Profile properties | `$set`, `$set_once`, and person properties on `$identify` |
To set subscriber attributes from your product, use PostHog's `identify` call. Passing `$set` or person properties on another event does not update the subscriber profile in Sequenzy.
## Using with Automations
### Feature Adoption Sequence
Trigger emails when users haven't used a key feature:
1. Go to **Automations** → **Create New**
2. Set trigger to **Event Received** → `product.signup_completed`
3. Add a **Wait** step: 3 days
4. Add a **Condition**: Check if `product.feature_used` was NOT received
5. Send a "Did you know about this feature?" email
### Onboarding Based on Product Usage
Send different onboarding based on what users do:
```
Trigger: product.signup_completed
├── Wait 1 day
├── Check: product.first_project_created received?
│ ├── Yes → Send "Great start!" email
│ └── No → Send "Need help getting started?" email
```
### Re-engagement for Inactive Users
Target users who stopped using the product:
1. Create a segment: "No `product.` events in last 14 days"
2. Send a re-engagement campaign to this segment
## Event Properties as Variables
Use PostHog event properties in your emails:
```
Hi {{firstName}},
We noticed you used {{featureName}} - here are some tips to get more out of it!
```
## Event Allowlist (Optional)
To sync only specific events:
1. Go to **Settings → Integrations**
2. Click the **gear icon** next to PostHog
3. Uncheck **Sync all events**
4. Pick events from the list, or add names manually
When your connection has a personal API key stored, the list shows the event
names your project actually emitted in the last 90 days, sorted by volume, so
you pick instead of typing. The same dialog controls whether PostHog-internal
`$` events are skipped.
## Troubleshooting
### Events Not Appearing
1. Verify the webhook URL is correct in PostHog
2. Check that the `X-Webhook-Secret` header matches your Sequenzy secret
3. **Most common issue**: Events don't have an email - call `posthog.identify()` with user email when they log in
### Subscriber Not Created
Events without email are silently skipped. Ensure:
1. You're calling `posthog.identify(userId, { email: '...' })` when users log in
2. The email is in one of these fields: `email`, `user_email`, `$email`, `userEmail`, `user.email` (nested), identify `$set.email`, or `person.properties.email`
3. The email is a valid format
### Anonymous Events Being Skipped
This is expected behavior. Sequenzy can't send emails to anonymous users, so we skip events without email addresses. Once a user is identified with `posthog.identify()`, all their subsequent events will be synced.
### Missing Subscriber Attributes
Subscriber attributes are updated only by `$identify` events. If an attribute isn't appearing on the subscriber, set it via `posthog.identify(userId, { ... })`; `$set`, `$set_once`, and person properties attached to other event types do not update the subscriber profile in Sequenzy. PostHog internal fields (starting with `$`) are always excluded.
## FAQ
Both work for real-time events! The webhook destination feature is available
in PostHog Cloud and self-hosted installations. History import is available
for PostHog Cloud (US and EU) only.
Yes. Open the connect modal again from **Settings → Integrations**, enable
**Import event history**, and reconnect with your API key. Already-imported
and already-received events are never duplicated.
Retry it - from the **Retry import** button, `sequenzy integrations sync`, or
`POST /v1/integrations/{id}/sync`. The import starts over from the beginning
and events already imported are deduplicated, so the retry only fills in what
is missing. Live webhook events are unaffected throughout.
No, never. Historical events populate segments, audiences, and the contact
timeline, but they do not start sequences, resume waiting steps, apply sync
rules, or record conversions. Only live events trigger automations.
They're silently skipped. Sequenzy needs an email to match events to
subscribers. Make sure you call `posthog.identify()` with the user's email
when they log in.
Yes, if you want them: add `$pageview` to your event allowlist (or disable
"Skip PostHog-internal events" in the integration settings) and they sync like
any other event, appearing as `product.$pageview`. New connections skip them
by default because pageview volume usually drowns out the events you automate
on.
Yes, you can configure event filtering in PostHog's webhook destination
settings, or use Sequenzy's event allowlist feature in the integration
settings.
Events are processed in real-time. Most events appear in Sequenzy within
seconds of being sent from PostHog.
Yes, PostHog integration is included at no extra cost.
# Segment Integration
Source: https://docs.sequenzy.com/integrations/segment
Stream Segment track and identify calls into Sequenzy to trigger email automations
Connect Segment to stream your existing analytics events into Sequenzy. Track calls become events you can trigger automations on, and identify calls keep subscriber profiles up to date - all one-way, without changing your instrumentation.
## What is Segment?
[Segment](https://segment.com) is a customer data platform (CDP) that collects events from your product once and routes them to your downstream tools. If you already send `analytics.track()` and `analytics.identify()` calls, Sequenzy becomes one more destination.
## Prerequisites
Sequenzy matches events to subscribers by email, so your identify calls need an email trait:
```javascript theme={null}
// When a user logs in, identify them with their email
analytics.identify(userId, {
email: "user@example.com",
firstName: "John",
plan: "pro",
});
```
Identifying once is enough: the identify call stores the `userId` and
`anonymousId` on the subscriber, and later track, page, and screen calls that
carry only one of those identifiers are matched back to the same subscriber.
Purely anonymous events (never identified, no email anywhere) are
automatically skipped.
## Connecting Segment
### Step 1: Get Your Webhook Configuration
1. Go to **Settings → Integrations** in Sequenzy
2. Find **Segment** under Product Analytics
3. Click **Connect**
4. Copy the **Webhook URL** and choose a **Shared Secret** (at least 16 characters)
### Step 2: Add a Webhooks Destination in Segment
1. In your [Segment workspace](https://app.segment.com), open **Catalog → Destinations**
2. Search for **"Webhooks (Actions)"** and add it to the source you want to stream
3. Configure the destination:
* **URL**: Paste your Sequenzy webhook URL
* **Shared Secret**: Enter the same secret you chose in Step 1
4. Enable mappings for the **Track** and **Identify** calls you want to send (Send as JSON, POST)
5. Leave **Batch Size** set to `0` so every signed request contains one message
6. Enable the destination
### Step 3: Complete Connection in Sequenzy
1. Return to Sequenzy's Segment connection modal
2. Enter the same shared secret
3. Click **Connect Segment**
That's it! Segment calls will now flow into Sequenzy. Deliveries are verified against the shared secret, and duplicate deliveries are deduplicated by Segment's own `messageId`.
Keep batching disabled in Segment. Webhooks (Actions) signs only the first
item of a batched delivery, so later items are not protected against
tampering. Sequenzy requires the signature to cover the complete request and
rejects Segment's first-item-only batch signature.
## What Gets Synced
| Segment call | What Sequenzy does |
| - | - |
| `track` | Records `product.` on the matched subscriber and triggers automations |
| `identify` | Creates or updates the subscriber from traits, then records `product.identify` |
| `page` | Records `product.page` - skipped by default on new connections (see below) |
| `screen` | Records `product.screen` - skipped by default on new connections |
| `group` | Acknowledged but never creates a subscriber (group traits describe an account, not a person) |
| `alias` | Acknowledged but never creates a subscriber or event |
When an event is received, Sequenzy:
1. Checks for an email in the event `properties`, or in `context.traits` for identified calls (identify calls use their `traits`)
2. If there is no email, matches the event's `userId` or `anonymousId` against the `segmentUserId` / `segmentAnonymousId` attributes stored by an earlier identify call - **only events that resolve neither way are skipped**
3. Creates or updates the subscriber (an identify reporting a new email for a known `userId` updates the existing subscriber's email instead of creating a duplicate)
4. Records the event with the `product.` prefix (e.g., `product.Order Completed`, `product.feature_used`)
5. Triggers any automations listening for that event
### Event Naming
Segment track events are prefixed with `product.` in Sequenzy, keeping the event name verbatim:
| Segment call | Sequenzy Event |
| - | - |
| `analytics.track("Order Completed")` | `product.Order Completed` |
| `analytics.track("feature_used")` | `product.feature_used` |
| `analytics.identify(...)` | `product.identify` |
| `analytics.page(...)` | `product.page` |
Segment and PostHog feed the same `product.*` namespace, so sequences built on
`product.*` events keep working if you switch analytics providers.
### Synced Attributes
Only `identify` calls update subscriber attributes. Properties on all other calls stay on the event (available to automations and event-based segments) and are never copied onto the subscriber profile:
| Attribute | Source |
| - | - |
| `segmentUserId` | The `userId` on the identify call |
| `segmentAnonymousId` | The `anonymousId` on the identify call, so pre-login events resolve too |
| First / last name | `firstName`/`lastName` traits, or a single `name` trait split on the first space |
| Custom attributes | Primitive (string, number, boolean) identify traits |
Credential-looking trait keys (`token`, `password`, `apiKey`, and similar) are always redacted before storage, and nested objects are not synced as attributes.
## Importing Event History (Optional)
A fresh connection only receives events from the moment the webhook goes live. If you use [Segment Unify](https://segment.com/docs/unify/), you can also backfill your existing contacts' recent event history so event-based segments reflect the last two weeks of activity immediately. Segment's Profile API serves at most the last 14 days of a profile's events, so that is the window the import covers - older history is not available from Segment.
Segment has no bulk event export, so the import works through the [Profile API](https://segment.com/docs/unify/profile-api/): Sequenzy walks your existing contacts and reads each one's profile history (last 14 days) by email. Contacts without a Unify profile are simply skipped, and imported events dedupe against live webhook deliveries by `messageId`, so the overlap window cannot double-count.
Historical events never start automations, never send emails, and never fire
outbound webhooks. They appear on the contact timeline and count toward
event-based segments, and a contact's signup date moves earlier when history
proves they predate it.
To set it up:
1. In Segment, go to **Unify → Settings → API access** and note the **Space ID**, then generate a **Profile API token**
2. In Sequenzy's Segment connection modal, check **Import event history** and enter both - the workspace region (US or EU) is detected automatically
3. Connect. The import runs in the background; the Segment card shows its progress and offers **Retry** if it fails
From the CLI:
```bash theme={null}
sequenzy integrations connect segment \
--webhook-secret my_shared_secret \
--history-region us \
--history-space-id spa_abc123 \
--history-api-key profile_api_token
```
A failed or interrupted import can be re-run at any time with `sequenzy integrations sync ` (or the dashboard's Retry button). Re-runs restart from the beginning and already-imported events dedupe, so nothing is duplicated.
## Page and Screen Calls
Connections created from August 2026 on skip `page` and `screen` calls by default because their volume usually drowns out the events you automate on - `identify` always passes so profile updates keep working. To ingest them, add `page` or `screen` to your event allowlist and make sure their mappings are enabled on the Segment destination (Segment only delivers calls whose mappings are on). If you never want them, disable their mappings in Segment instead - that also keeps them out of your Segment delivery volume.
## Event Allowlist (Optional)
To sync only specific events, connect via the API, CLI, or MCP with an allowlist:
```bash theme={null}
sequenzy integrations connect segment \
--webhook-secret my_shared_secret \
--event-allowlist "Order Completed" "signed_up"
```
`identify` calls always pass, even when they are not on the allowlist - they carry the profile updates that keep every other event resolvable.
Or filter at the source: Segment's destination filters and mapping triggers control which calls are delivered at all, which also keeps them out of your Segment delivery volume.
## Using with Automations
### Activation Sequence
1. Go to **Automations** → **Create New**
2. Set trigger to **Event Received** → `product.signed_up`
3. Add a **Wait** step: 3 days
4. Add a **Condition**: Check if `product.feature_used` was NOT received
5. Send a "Did you know about this feature?" email
### Re-engagement for Inactive Users
1. Create a segment: "No `product.` events in last 14 days"
2. Send a re-engagement campaign to this segment
## Troubleshooting
### Events Not Appearing
1. Verify the destination URL in Segment matches your Sequenzy webhook URL exactly
2. Check that the destination's **Shared Secret** matches the secret you entered in Sequenzy - mismatches are rejected with a 401 and show up as failed deliveries in **Settings → Integrations → Activity**
3. **Most common issue**: Events don't carry an email - call `analytics.identify()` with the user's email when they log in
### Subscriber Not Created
Events that resolve neither an email nor a previously identified `userId` are silently skipped. Ensure:
1. You're calling `analytics.identify(userId, { email: "..." })` when users log in
2. The email is in `traits.email` (identify), `properties.email`, or `context.traits.email`
3. The email is a valid format
### Missing Subscriber Attributes
Subscriber attributes are updated only by `identify` calls. If an attribute isn't appearing, set it as an identify trait; properties on track calls never update the profile. Nested objects and credential-looking keys are excluded.
## FAQ
One-way: Sequenzy ingests events from Segment and never writes anything
back. Sequenzy is a destination, not a source.
The Webhooks (Actions) destination is available on all Segment plans,
including the free tier.
Yes. Add the same Webhooks destination (same URL and shared secret) to each
source you want to stream.
Once a user has been identified with an email, later events that carry only
their `userId` or `anonymousId` are matched back to the same subscriber.
Events from users who were never identified (and carry no email anywhere) are
silently skipped - Sequenzy needs an email or a known identifier to match a
subscriber.
No. Deliveries are deduplicated by Segment's `messageId`, so a retry of the
same message is acknowledged without creating a second event.
Events are processed in real-time. Most events appear in Sequenzy within
seconds of being delivered by Segment.
Yes, the Segment integration is included at no extra cost.
# Shopify Integration
Source: https://docs.sequenzy.com/integrations/shopify
Connect Shopify to sync products, customers, orders, storefront browse events, back-in-stock requests, and replenishment reminders.
You can connect a Shopify store to Sequenzy from **Settings → Integrations**.
Once connected, Sequenzy syncs store data, listens for Shopify webhooks, and uses the provider-neutral commerce event pipeline to capture known-customer storefront browse events. Those events can power product-aware automation such as replenishment reminders, back-in-stock emails, and personalized recommendations.
## Connect Shopify
Open **Settings → Integrations**, search for **Shopify**, and choose **Install
from Shopify**. Complete the Shopify authorization flow, then return to
Sequenzy to configure subscriber sync and store automations.
## What It Does
* Sync products, variants, prices, images, stock state, and product URLs
* Keep the catalog fresh in real time through product create/update/delete webhooks
* Sync Shopify customers and order revenue onto subscriber profiles
* Track order, checkout, customer, refund, fulfillment, and stock events
* Track shipment stages (shipped, out for delivery, delivered, delivery failed) with tracking data
* Reverse subscriber revenue and campaign attribution when orders are refunded or cancelled
* Track known-customer product views, add-to-cart, collection views, and searches through the Shopify Web Pixels API
* Trigger browse abandonment events when a viewer doesn't buy within the browse window
* Trigger price drop events for recent viewers when a product's price decreases
* Capture back-in-stock requests from the Shopify storefront block
* Trigger replenishment reminder events after a product-specific delay
* Create dynamic Shopify discount codes from sequence discount actions
* Expose `recommendedProducts` merge variables for product recommendation blocks
* Show purchased products on subscriber profiles alongside Stripe products
## Required Scopes
Sequenzy requires these Shopify OAuth scopes:
| Scope | Used For |
| - | - |
| `read_customers` | Customer sync and customer profile updates |
| `read_orders` | Order events, revenue, and purchased items |
| `read_fulfillments` | Shipment tracking events |
| `read_products` | Product catalog, variants, pricing, images |
| `read_inventory` | Inventory-level updates for stock webhooks |
| `read_checkouts` | Checkout events for recovery and automation |
| `read_discounts` | Discount lookup and validation |
| `write_discounts` | Discount code creation |
| `read_pixels` | Web pixel lookup during Shopify sync |
| `write_pixels` | Web pixel activation and settings updates |
| `read_customer_events` | Shopify Web Pixels customer events |
When you reconnect Shopify, Sequenzy checks the scopes Shopify actually granted to the saved token. If any required scope is missing, reconnect the store from Settings so Shopify can grant the latest permissions.
## Storefront Browse Tracking
Sequenzy includes a Shopify web pixel extension that subscribes to Shopify's `product_viewed`, `product_added_to_cart`, `product_removed_from_cart`, `cart_viewed`, collection, and search standard events. A known shopper's `cart_viewed` event is normalized to `ecommerce.cart_started` with the complete cart lines, quantities, product metadata, images, and prices required by recovery emails. Product removals update the tracked cart snapshot; removing the final line emits `ecommerce.cart_cleared`. The pixel is one adapter for the generic commerce customer event ingestion endpoint. During Shopify post-connect sync, Sequenzy creates or updates the store's app pixel settings with this endpoint:
```text theme={null}
https://api.sequenzy.com/api/webhooks/commerce/shopify/{companyId}/customer-events
```
The previous Shopify-specific endpoint, `/api/webhooks/shopify/{companyId}/customer-events`, remains available as a compatibility alias. The managed Shopify pixel includes a signed verification token in each request; the endpoint validates the token, connected company, and shop, queues the event, and returns immediately. A worker records browse events for known subscribers with an email in the pixel event context.
### Checking and Activating the Pixel
Everything in this section depends on the pixel being installed on the store. When it is not, none of these events arrive, and a sequence triggered by one of them enrolls nobody without reporting an error - so check the pixel before trusting browse abandonment, cart recovery, product views, collection views, or storefront search.
Pixel state is not stored by Sequenzy: a merchant can remove the pixel from the Shopify admin at any time, so it is always read from the store.
```bash theme={null}
# Is on-site tracking live?
curl "https://api.sequenzy.com/api/v1/integrations/int_abc123/pixel" \
-H "Authorization: Bearer YOUR_API_KEY"
# Install it, or repoint an existing pixel at this account
curl -X POST "https://api.sequenzy.com/api/v1/integrations/int_abc123/pixel" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The same actions are available as `sequenzy integrations pixel ` and `sequenzy integrations enable-pixel ` on the CLI, and as `get_integration_pixel` and `activate_integration_pixel` over MCP. See [Get Storefront Pixel](/api-reference/integrations/pixel-get) and [Activate Storefront Pixel](/api-reference/integrations/pixel-activate).
Activation is idempotent, and events start arriving on the next storefront visit - nothing is backfilled for the period the pixel was off, so turn it on before building the sequence that depends on it. If the store granted an older permission set, activation fails with a message naming the reconnect step rather than silently doing nothing; reconnect Shopify from Settings to grant `write_pixels`, then retry.
### Anonymous Visitor Backfill
When a shopper browses without an identity, the pixel buffers their events (product views, add-to-carts, cart views, collection views, and searches) in the browser for up to 14 days. The moment the shopper identifies - entering their email at checkout, or visiting while signed in to a customer account - the buffered events are replayed to Sequenzy with `backfill: true` and recorded on the contact's activity timeline at their original timestamps. Backfilled events never trigger event-received sequences, never resume a sequence the contact is already waiting in, and never schedule cart- or browse-abandonment checks; they exist to complete the contact's history, segments, and product-interest data. They do count toward segment membership, so a contact can still enter a segment-triggered sequence when the backfilled activity makes them match that segment - scope such segments to a recent time window (for example "viewed a product in the last 3 days") if you do not want replayed history to enroll them. The identified email is also remembered in that browser for 30 days, so subsequent visits are tracked live and do power abandonment automations. Buffering runs inside Shopify's consent-gated pixel sandbox, so it honors the store's customer privacy settings, and events replayed before the contact record exists are retried for up to 15 minutes while the checkout webhook lands.
Custom storefront adapters can post the same shape to:
```text theme={null}
https://api.sequenzy.com/api/webhooks/commerce/{provider}/{companyId}/customer-events
```
Use one of the supported product providers as `{provider}` (`shopify`, `woocommerce`, `manual`, `api`, or `stripe`). Shopify and WooCommerce payloads must include a valid signed `verificationToken` for the active integration. Generic adapters such as `api`, `manual`, or `stripe` must include a Sequenzy API key in `Authorization: Bearer ...` or `x-api-key`; the key must belong to the route company. Generic `api` adapters can send events without Shopify-specific fields.
Browse events trigger these internal events:
| Shopify Event | Sequenzy Event |
| - | - |
| `product_viewed` | `ecommerce.product_viewed` |
| `product_added_to_cart` | `ecommerce.product_added_to_cart` |
| `product_removed_from_cart` | Updates the tracked cart; `ecommerce.cart_cleared` when empty |
| `cart_viewed` | `ecommerce.cart_started` |
| `collection_viewed` | `ecommerce.collection_viewed` |
| `search_submitted` | `ecommerce.search_submitted` |
Product events include product matching fields for automations:
```json theme={null}
{
"provider": "shopify",
"source": "shopify_web_pixel",
"providerAccountId": "store.myshopify.com",
"providerEventName": "product_viewed",
"providerProductId": "788032119674292922",
"providerVariantId": "45123456789123",
"productTitle": "Protein Powder",
"variantTitle": "Vanilla",
"product": {
"provider": "shopify",
"providerProductId": "788032119674292922",
"providerVariantId": "45123456789123",
"title": "Protein Powder",
"imageUrl": "https://cdn.example.com/product.png",
"url": "https://store.example.com/products/protein-powder",
"price": 88.5,
"priceCents": 8850,
"currency": "USD"
}
}
```
Cart events include `cartId`, `itemCount`, `totalQuantity`, `totalPriceCents`,
`currency`, `productNames`, and normalized `lineItems`. Each line item contains
the product and variant IDs, title, quantity, formatted price, image URL, and
product URL used by dynamic recovery-email blocks.
## Personalized Recommendations
Campaign, test-email, sequence, and A/B test sends receive a generated `recommendedProducts` merge variable. The MVP ranks products across commerce providers in this order:
1. Recent products the subscriber viewed or added to cart
2. Products browsed by similar customers who viewed the same products
3. Popular recently browsed products from the same connected catalog
4. Recent in-stock catalog products
Products the subscriber already purchased are excluded when the purchase event contains commerce line items with provider and product IDs.
Use these variables in email content:
```text theme={null}
{{recommendedProducts.0.title}}
{{recommendedProducts.0.imageUrl}}
{{recommendedProducts.0.price}}
{{recommendedProducts.0.url}}
```
The same array is also available under `{{variables.recommendedProducts.0.title}}`.
## Synced Data
### Products
The product catalog includes:
* Product title, description, handle, image, URL, and stock state
* Lowest product price and compare-at price when available
* Variant IDs, titles, SKUs, option values, prices, images, and inventory state
* Replenishment settings stored per product
You manage synced products from **Settings → Products**. Product rows show stock, price, variants, back-in-stock waitlists, and pending replenishment reminders.
### Subscribers
Shopify customer and order data is stored as subscriber attributes:
| Attribute | Type | Description |
| - | - | - |
| `ordersCount` | Number | Total known Shopify orders |
| `totalOrders` | Number | Alias for total known Shopify orders |
| `totalSpent` | Number | Total known Shopify spend in store money |
| `ltv` | Number | Lifetime value used for segmentation |
| `aov` | Number | Average order value |
Older subscribers may still have `totalRevenue` or `averageOrderValue`. Sequenzy treats those as legacy fallbacks for `ltv` and `aov` instead of showing duplicate fields in subscriber views.
### Phone and SMS consent
Customer sync and customer webhooks also carry each customer's phone number
and SMS marketing consent. A customer who opted into SMS marketing on your
store (for example at checkout) becomes SMS-subscribed in Sequenzy with
consent source `shopify` and Shopify's consent timestamp; an opt-out on
Shopify unsubscribes them. A phone number without an explicit opt-in is
stored but never treated as consent. This requires approved phone field
access for protected customer data on your store - without it, the sync
skips phone and consent and everything else still works. See
[SMS consent](/concepts/sms#shopify-sms-consent-sync) for details.
## Order Events
Shopify order webhooks trigger `ecommerce.order_placed` with product line item data:
```json theme={null}
{
"orderId": "1002",
"totalPriceCents": 8850,
"currency": "USD",
"orderedAt": "2026-04-02T00:00:00.000Z",
"lineItems": [
{
"provider": "shopify",
"lineItemId": "line-1",
"providerProductId": "788032119674292922",
"providerVariantId": "45123456789123",
"sku": "VANILLA-1",
"title": "Protein Powder",
"variantTitle": "Vanilla",
"quantity": 1,
"priceCents": 8850
}
]
}
```
Sequenzy uses `lineItems[].providerProductId` and, when variant-level matching is enabled, `lineItems[].providerVariantId` to match follow-up automation against the exact product that was purchased.
Checkout webhooks trigger `ecommerce.checkout_started` once per checkout, as soon as the buyer's email is known. The event carries the line items at that moment plus `abandonedCheckoutUrl` so recovery emails can link straight back to the checkout. Completed checkouts never trigger the event - the order webhook takes over.
## Shipment Events
Fulfillment webhooks track each shipment through its delivery lifecycle. Every stage triggers exactly once per fulfillment:
| Sequenzy Event | When |
| - | - |
| `ecommerce.order_shipped` | A fulfillment is created or confirmed in transit |
| `ecommerce.order_out_for_delivery` | Carrier reports the shipment is out for delivery |
| `ecommerce.order_delivered` | Carrier reports the shipment as delivered |
| `ecommerce.delivery_failed` | Carrier reports a failed or attempted delivery |
Events include `trackingNumber`, `trackingUrl`, `trackingCompany`, `shipmentStatus`, and the fulfilled `lineItems`, so shipping confirmation emails can render tracking links and review requests can wait for the actual delivery date. Carrier stage updates (`out_for_delivery`, `delivered`) require a Shopify-supported tracking carrier; untracked fulfillments still trigger `ecommerce.order_shipped`. Existing stores must reconnect once so Sequenzy can request the `read_fulfillments` scope.
## Refunds and Cancellations
Refund and cancellation webhooks keep revenue honest:
* `refunds/create` subtracts the refunded amount from the subscriber's `totalSpent`, `ltv`, and `aov` attributes
* `orders/cancelled` also decrements `ordersCount`, for orders Sequenzy previously counted
* Attributed campaign and sequence revenue is adjusted with a matching negative conversion, so dashboards net out refunded orders instead of overstating
## Browse Abandonment
When a known subscriber views a product through the browse pixel and then goes quiet, Sequenzy triggers `ecommerce.browse_abandoned` after a configurable delay (default 2 hours). The check is skipped when the subscriber placed any order, started a checkout, or added the viewed product to their cart since the view, and at most one browse abandonment event fires per subscriber per cooldown window (default 24 hours). The event carries the viewed product so recovery emails can show exactly what caught their eye.
## Price Drop Alerts
When a product update lowers a variant's price by at least 5% (configurable), Sequenzy triggers `ecommerce.price_drop` for subscribers who viewed the product or added it to their cart in the last 30 days. Events include `oldPriceCents`, `newPriceCents`, `percentOff`, and the product payload for rendering. Each subscriber receives at most one price drop event per product per 7 days.
If no sequence is set up for `ecommerce.price_drop`, Sequenzy sends a default price-drop email automatically - a product card with the old price struck through, the new price, the discount percent, and a link to the product. Active subscribers only; unsubscribed contacts never receive it. Create a sequence triggered by `ecommerce.price_drop` to replace the default email with your own.
## Automation Settings
Browse abandonment and price drop are on by default with sensible timing. Adjust or disable them per store:
* **Dashboard**: **Settings → Integrations → Shopify → Settings**
* **CLI**: `sequenzy shopify settings get` / `sequenzy shopify settings update`
* **MCP**: `get_shopify_automation_settings` / `update_shopify_automation_settings`
* **API**: `GET`/`PUT /api/v1/shopify/automation-settings`
Settings survive store reconnects and token refreshes.
## Replenishment Reminders
Replenishment reminders start from synced products:
1. Open **Settings → Products**.
2. Choose a Shopify product.
3. Turn on replenishment.
4. Pick how many days after purchase the product usually runs out.
5. Choose whether matching should use any variant of the product or the exact purchased variant.
When someone buys a configured product, Sequenzy queues a reminder. If the subscriber buys the same product again before the reminder is due, the reminder is skipped. When the reminder fires, it triggers `ecommerce.replenishment_due`.
The event includes the same product matching fields:
```json theme={null}
{
"provider": "shopify",
"providerProductId": "788032119674292922",
"providerVariantId": "45123456789123",
"replenishmentScope": "variant",
"orderedAt": "2026-04-02T00:00:00.000Z",
"sourceOrderId": "1002",
"product": {
"providerId": "788032119674292922",
"providerVariantId": "45123456789123",
"title": "Protein Powder",
"variantTitle": "Vanilla",
"imageUrl": "https://cdn.example.com/product.png",
"url": "https://store.example.com/products/protein-powder",
"price": "$88.50",
"priceCents": 8850,
"currency": "USD"
}
}
```
Sequences started by `ecommerce.replenishment_due` get a default stop condition: they stop when the same product is purchased again.
## Enable the Back in stock theme block
After installing the Shopify app, add the Sequenzy theme app block to a product template:
1. In Sequenzy, open **Settings → Integrations → E-Commerce → Shopify**.
2. Click **Add Back in stock block**. This opens Shopify's theme editor on the product template with the app block ready to add.
3. In the Shopify theme editor, add **Back in stock** from **Apps** or **Sequenzy Notify Me**.
4. Place it near the product form, click **Save**, and preview an out-of-stock product or unavailable variant.
Sequenzy uses a product-page app block for back-in-stock capture. No separate app embed is required for this theme extension.
The block is hidden on in-stock variants by default. It appears when the selected variant is unavailable, unless you change the block's **Show block when** setting.
If the direct button is not available, use this manual path in Shopify:
`Online Store -> Themes -> Customize -> Products -> Default product -> Add block -> Apps -> Back in stock`
The deep link format is:
```text theme={null}
https://{shop}.myshopify.com/admin/themes/current/editor?template=product&addAppBlockId=81144e992d5cb6078bb79fbeb74236a2%2Fback-in-stock&target=newAppsSection
```
## Back-In-Stock Requests
The Shopify storefront block captures customer email, product ID, variant ID, product title, and variant title for out-of-stock items. Sequenzy records the request and triggers `ecommerce.back_in_stock_requested`.
When a synced product variant returns to stock, Sequenzy marks active waitlist entries as notified and triggers `ecommerce.back_in_stock`.
```json theme={null}
{
"provider": "shopify",
"providerProductId": "788032119674292922",
"providerVariantId": "45123456789123",
"productTitle": "Protein Powder",
"variantTitle": "Vanilla",
"productUrl": "https://store.example.com/products/protein-powder",
"imageUrl": "https://cdn.example.com/product.png",
"requestedAt": "2026-04-01T12:00:00.000Z",
"backInStockAt": "2026-04-02T08:00:00.000Z"
}
```
Sequences started by `ecommerce.back_in_stock` get a default stop condition: they stop if the same item is purchased or if the same item goes out of stock again.
## Subscriber Profiles
Subscriber profiles show commerce context from Shopify:
* Purchased products from order line items, including product image, variant, quantity, price, and last purchase time
* Pending replenishment reminders
* Active back-in-stock waitlist entries
* Revenue metrics such as `ltv` and `aov`
Revenue metrics come from Shopify customer totals when available. Purchased
product rows come from order line items, so they can be less complete than LTV
and AOV if Shopify does not grant all historical order access. By default,
Shopify order objects expose only the last 60 days unless the app is approved
for `read_all_orders`.
Pending replenishment rows link to the subscriber profile when the subscriber is known.
# Shopify App Integration
Source: https://docs.sequenzy.com/integrations/shopify-app
Track installs, app subscriptions, purchases, and uninstalls from your Shopify app
Use the Shopify App integration when you build a Shopify app and want Sequenzy to email your app's merchants. This is different from connecting a Shopify store.
## What it tracks
Sequenzy receives signed Shopify app webhooks and turns them into subscriber events:
| Shopify event | Sequenzy event |
| - | - |
| App identified after OAuth | `shopify_app.installed` |
| Active app subscription | `saas.purchase` plus monthly/yearly variant |
| Active one-time app purchase | `saas.purchase` |
| Frozen app subscription | `saas.payment_failed` |
| Cancelled/expired/declined app subscription | `saas.churn` |
| App uninstalled | `saas.churn` |
| Approaching capped amount | Acknowledged, attributes updated when possible |
Subscription and purchase events also update revenue attributes such as `mrr` and `ltv`.
## Connect in Sequenzy
1. Go to **Settings → Integrations**.
2. Click **Connect** next to **Shopify App**.
3. Enter your Shopify app's **App client ID** and **App client secret**. In the Shopify Dev Dashboard, open **Apps > your app > Settings** and copy both from the **Client credentials** section (on the older Partner Dashboard they are on the app's **Overview** page instead). Shopify sometimes labels these the **API key** and **API secret key** - they are the same values.
4. Copy the generated **webhook URL** and **identify URL** and add them to your Shopify app configuration.
5. Click **Connect Shopify App**.
The client secret is encrypted and used only to verify Shopify webhook HMAC signatures.
## Configure Shopify app webhooks
In your Shopify app configuration or Partner Dashboard, subscribe to these topics:
```toml theme={null}
[[webhooks.subscriptions]]
topics = [
"app/uninstalled",
"app_subscriptions/update",
"app_subscriptions/approaching_capped_amount",
"app_purchases_one_time/update"
]
uri = "https://api.sequenzy.com/api/webhooks/shopify-app/{companyId}"
```
Shopify signs webhook deliveries with `X-Shopify-Hmac-SHA256`. Sequenzy verifies that signature with your app client secret before processing the event.
## Identify the merchant after OAuth
Shopify app billing webhooks usually do not include a merchant email. After your app completes OAuth, call the identify endpoint from your backend:
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/webhooks/shopify-app/{companyId}/identify" \
-H "Authorization: Bearer YOUR_SHOPIFY_APP_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"shopDomain": "merchant.myshopify.com",
"email": "owner@example.com",
"shopName": "Merchant Store",
"appId": "your-shopify-app-client-id",
"planName": "Pro"
}'
```
This creates or updates the subscriber, stores `shopifyAppShopDomain`, and records a `shopify_app.installed` event. Later subscription and uninstall webhooks use the shop domain to find the same subscriber.
## Backfill existing merchants
Shopify never replays installs, so connecting only tracks merchants from that point forward. To sync the merchants you already have, send them in bulk from your backend. Each merchant is created or updated as a subscriber, its Shopify app attributes plus `mrr` and `ltv` are set, and a `shopify_app.installed` event is recorded. The backfill does not trigger automations, so it will not email your existing merchants.
```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/webhooks/shopify-app/{companyId}/identify/bulk" \
-H "Authorization: Bearer YOUR_SHOPIFY_APP_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"merchants": [
{
"email": "owner@store.com",
"shopDomain": "store.myshopify.com",
"shopName": "Acme Store",
"planName": "Pro",
"interval": "month",
"planPrice": 49,
"status": "active",
"mrr": 49,
"ltv": 588
}
]
}'
```
Send up to 1000 merchants per request and page through the rest. Each merchant needs a valid `email` and a `myshopify.com` `shopDomain`; everything else is optional.
| Field | Description |
| - | - |
| `email` (required) | Merchant contact email |
| `shopDomain` (required) | Shop domain, for example `merchant.myshopify.com` |
| `shopName` | Store display name |
| `name` / `firstName` / `lastName` | Merchant name |
| `planName` | Current plan, for example `Pro` |
| `interval` | `month`, `year`, or `one_time` (defaults to `month`) |
| `planPrice` | Plan price per interval, in major units (for example dollars) |
| `status` | `active`, `frozen`, `cancelled`, or `one_time` (defaults to `active`) |
| `mrr` | Monthly recurring revenue. Derived from `planPrice` + `interval` when omitted |
| `ltv` | Lifetime value. Falls back to `planPrice` when omitted; pass it for accurate history |
| `subscriptionId` | Shopify app subscription or one-time purchase id |
| `installedAt` | ISO install timestamp |
The request is queued and processed in the background. The response returns `queued` and `skipped` counts (rows missing a valid email or shop domain are skipped).
## Synced attributes
| Attribute | Description |
| - | - |
| `shopifyAppShopDomain` | Merchant shop domain, for example `merchant.myshopify.com` |
| `shopifyAppStoreName` | Store name supplied during identify or webhook parsing |
| `shopifyAppId` | Shopify app client ID supplied during identify |
| `shopifyAppPlanName` | Current app plan name |
| `shopifyAppSubscriptionId` | Shopify app subscription or one-time purchase ID |
| `shopifyAppSubscriptionStatus` | Latest app subscription status |
| `shopifyAppPurchaseStatus` | Latest one-time app purchase status |
| `shopifyAppInterval` | `month`, `year`, or `one_time` |
| `mrr` | Monthly recurring revenue in dollars |
| `ltv` | Lifetime value in dollars |
## Automation ideas
* Start onboarding from `shopify_app.installed`.
* Send plan-specific activation emails from `saas.purchase`.
* Send usage-cap warnings from segments using `shopifyAppCappedAmount` and `shopifyAppBalanceUsed`.
* Trigger win-back or exit surveys from `saas.churn`.
* Trigger dunning or support outreach from `saas.payment_failed`.
## Troubleshooting
### Webhook says no customer email found
Call the identify endpoint after OAuth for that shop. Shopify app billing webhooks are matched by `shopifyAppShopDomain`, not by email.
### Invalid webhook signature
Verify that the secret saved in Sequenzy is the Shopify app client secret from the Partner Dashboard. Shopify signs the raw request body, so proxy layers must not rewrite the body before it reaches Sequenzy.
### Install event not firing
Shopify does not send a general `app/installed` webhook. The install event comes from your backend calling the identify endpoint after OAuth.
# Stripe Integration
Source: https://docs.sequenzy.com/integrations/stripe
Sync revenue data from Stripe to power segments and automations
Connect your Stripe account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## Connecting Stripe
1. Go to **Settings → Integrations**
2. Click **Connect** next to Stripe
3. Authorize Sequenzy to access your Stripe account
4. Review the Stripe setup step and save your sync settings
## Setup Options
Before the first sync starts, you can control how Stripe data should flow into Sequenzy:
* **Products to sync**: You choose which Stripe products count as relevant. For paid customers, Sequenzy syncs people who made a payment in one of those products.
* **Sync users without any payment ever**: You can keep Stripe users with a real trial or subscription in sync before their first payment. Customers created from abandoned payment pages stay excluded. If you also selected products, those users still need to be tied to one of the selected products.
* **Sync customers with no subscription or payment**: You can also import Stripe customers that have never started a trial, subscription, or payment - for example free-tier users your app creates at signup. This is off by default, and it only appears while **Sync users without any payment ever** is on, because it widens that setting rather than replacing it. Enable it only if your app creates Stripe customers itself: abandoned Stripe Checkout payment pages can leave customer records behind, and those emails would be imported too. Product selections do not apply to these customers because they have no product to match. Customers whose only subscription is an abandoned incomplete checkout stay excluded. Unlike trial and paid customers, these customers have no Stripe webhook event that adds them, so they only arrive through the sync itself: it runs when you save these settings, and you can run it again with **Sync Revenue** in **Settings → Integrations**.
* **Products to watch in webhooks**: You can limit webhook-driven updates to a subset of the synced products. Products removed from sync are automatically disabled here too.
* **Lists for new subscribers**: You choose which lists new Stripe-synced subscribers should join. The default is your workspace **Default Lists** setting. Switch to **Specific lists** to target a curated set, or select nothing to keep Stripe-synced subscribers out of every list. Existing subscribers are never moved by this setting.
* **Extra metadata to carry over**: You can pick suggested metadata keys from sampled Stripe customer and subscription records, or add any custom keys you want to copy into subscriber attributes.
If Stripe is already connected, open **Settings → Integrations** and click the gear icon next to Stripe to update these settings later.
## Discount Code Automations
Sequences can include dynamic discount steps that create Stripe coupons and subscriber-specific promotion codes when each subscriber reaches the step, before later emails send. These steps require write access to your Stripe account.
If you connected Stripe before discount code support was added, reconnect Stripe from **Settings → Integrations** so Sequenzy can request the updated coupon and promotion-code permissions.
### Initial Sync
When you connect Stripe, Sequenzy performs a full sync of your entire customer base. This means:
* **Eligible customers** are matched to subscribers by email based on your Stripe setup settings
* **MRR and LTV** are calculated for each matched subscriber
* **Tags are applied** based on current subscription status
* **Historical data** is available immediately: no waiting period
The initial sync processes customers in batches, so large customer bases are handled efficiently without API rate limits.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
### MRR Calculation
MRR is normalized from any billing interval:
* **Monthly**: Used as-is
* **Yearly**: Divided by 12
* **Weekly**: Multiplied by 52, then divided by 12
* **Daily**: Multiplied by 365, then divided by 12
Only `active` and `trialing` subscriptions count toward MRR.
### LTV Calculation
LTV sums all successful charges for a customer, minus any refunds. This gives you the true lifetime value accounting for partial refunds.
One-time purchases made through Stripe Checkout or Payment Links are included
when the Checkout Session is attached to a Stripe Customer. Guest Checkout
Sessions without a Stripe Customer are not part of the customer-scoped revenue
sync.
## Automatic Tags
Sequenzy automatically applies tags to subscribers based on their Stripe subscription status:
| Tag | Applied When |
| - | - |
| `customer` | Has an active or past\_due subscription |
| `trial` | Currently in a trial period |
| `past-due` | Payment failed, subscription past due |
| `cancelled` | Subscription set to cancel at period end |
| `churned` | Subscription has ended (no active subscription) |
| `saas.monthly` | On a monthly billing interval |
| `saas.yearly` | On a yearly billing interval |
These tags update automatically whenever subscription status changes via webhooks.
## Using Revenue Data
### Complex Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100`: High MRR customers
* `ltv > 1000`: Lifetime high-value customers
* `mrr >= 50 AND mrr < 200`: Mid-tier customers
**Combining with tags:**
* `tag = churned AND ltv > 500`: High-value churned customers (win-back targets)
* `tag = past-due AND mrr >= 100`: At-risk high-value customers needing attention
* `tag = trial AND mrr = 0`: Trial users who haven't converted yet
* `tag = saas.yearly AND mrr >= 100`: Yearly enterprise customers
**Combining with engagement:**
* `mrr >= 50 AND opened_emails_last_30_days = 0`: Paying but disengaged customers
* `ltv > 500 AND clicked_emails_last_30_days > 0`: Engaged high-value customers
### Automations
Trigger automations when revenue attributes change:
* Send a thank-you email when `ltv` exceeds a threshold
* Trigger a win-back sequence when a subscriber gets the `churned` tag
* Notify your team when a high-value customer's payment fails (`past-due` tag)
Stripe lifecycle webhooks and revenue syncs can also trigger built-in events:
| Event | Triggered when |
| - | - |
| `saas.purchase` | A subscription starts or renews, or a one-time payment completes |
| `saas.purchase.monthly` | The purchase was a monthly subscription |
| `saas.purchase.yearly` | The purchase was an annual subscription |
| `saas.trial_started` | A subscription begins with a trial period |
| `saas.trial_will_end` | Stripe warns a trial is about to end (usually 3 days out) |
| `saas.trial_ended` | A trial converts to a paid subscription |
| `saas.trial_cancelled` | A trialing subscription is set to cancel before it converts to paid |
| `saas.cancelled` | A subscription is scheduled to cancel |
| `saas.churn` | A subscription ends and the customer no longer has active access |
| `saas.payment_failed` | A subscription invoice payment fails |
| `saas.upgrade` | A subscription changes to a higher-priced plan |
| `saas.downgrade` | A subscription changes to a lower-priced plan |
| `saas.refund` | A charge is refunded |
| `saas.checkout_expired` | A Checkout Session expires without completing |
`saas.trial_will_end` comes from Stripe's
`customer.subscription.trial_will_end` webhook, normally about 3 days before a
trial ends. Stripe also triggers it for trials shorter than 3 days, so do not
assume every event arrives exactly 72 hours before the end. To email users
whose trial ended without a purchase, trigger a sequence on
`saas.trial_will_end`, delay for the remaining trial duration used by your
Stripe setup, and set the sequence's stop condition to `event_received` with
value `saas.purchase` (or add a condition step that checks the `customer` tag)
so converts never receive the recovery email. A fixed 3-day delay is
appropriate only when the event arrives 3 days before the end.
`saas.trial_ended` fires when a trial converts to paid, not when it expires.
### Failed Payment Recovery
`saas.payment_failed` events and the `saas.purchase` event sent when a
subscription invoice is paid include the Stripe invoice they belong to:
| Property | Description |
| - | - |
| `invoiceId` | Stripe invoice ID (`in_...`). It stays the same for every attempt on an invoice and for the payment of it |
| `subscriptionId` | Stripe subscription the invoice bills, when there is one |
| `attemptCount` | Stripe's `attempt_count`: the first attempt is `1` and each automatic retry adds one |
| `billingReason` | Stripe's `billing_reason`, such as `subscription_cycle` |
| `hostedInvoiceUrl` | Stripe's hosted invoice page, where the customer can pay the invoice or update their card |
`saas.payment_failed` also carries what a recovery email needs:
| Property | Description |
| - | - |
| `amountDue` | What is still unpaid on the invoice, in the smallest currency unit, such as cents |
| `amountDueFormatted` | `amountDue` formatted for display, such as `$49.00` |
| `currency` | Invoice currency, such as `usd` |
| `nextPaymentAttempt` | When Stripe will retry the payment automatically, as an ISO 8601 timestamp |
| `willRetry` | `false` when Stripe has no automatic retry scheduled: after the final retry, or for invoices you send for manual payment (`send_invoice`) |
For example, write `Your {{event.amountDueFormatted}} payment didn't go through`
and link the button to `{{event.hostedInvoiceUrl}}`, or add a condition on
`willRetry` to send a last-chance email once Stripe stops retrying.
A property is left out when Stripe does not send it. Purchases that do not come
from a paid invoice, such as one-time Checkout payments, have no `invoiceId`.
Events received before these properties were added do not have them either.
`saas.purchase` is sent only for subscription invoices (`billingReason`
`subscription_create`, `subscription_cycle` or `subscription_update`), while
`saas.payment_failed` is sent for any failed invoice. To run one recovery sequence per
failed subscription invoice, use this setup:
1. Trigger on `saas.payment_failed` with two property filters: `attemptCount`
equals `1`, so automatic retries of the same invoice never start another
run, and `billingReason` is one of the three subscription values above, so
every run can be stopped by a payment.
2. Set the enrollment mode to `matching_field` with the field path
`invoiceId`, so two invoices that fail at the same time each get their own
run.
3. Set the stop condition to `event_received` `saas.purchase`, matching
`invoiceId` on the enrolling event to `invoiceId` on the purchase.
Paying the invoice then stops only that invoice's run, and a later failed
invoice starts a new one.
In the sequence builder, when the trigger is `saas.payment_failed` and Stripe is
your only connected payment provider sending that event, the trigger panel
shows a **One run per failed invoice** switch. It starts off. Turning it on
fills in all three settings as unsaved changes, and turning it off clears them;
nothing is stored until you save. From the CLI, API or MCP, send the settings
explicitly:
```bash theme={null}
sequenzy sequences create invoice-recovery \
--trigger event_received \
--event-name saas.payment_failed \
--property-filters-json '[{"path":"attemptCount","operator":"equals","value":1},{"path":"billingReason","operator":"one_of","value":["subscription_create","subscription_cycle","subscription_update"]}]' \
--enrollment-mode matching_field \
--enrollment-field-path invoiceId \
--steps-file ./recovery-steps.json
sequenzy sequences update seq_123 --stop-condition-json '{"type":"event_received","value":"saas.purchase","matchConfig":{"mode":"event_property","rules":[{"entryFieldPath":"invoiceId","eventFieldPath":"invoiceId"}]}}'
```
The stop condition is checked when a run reaches its next step, so a payment
ends the run before its next email rather than the moment it arrives. It only
counts payments received after the run started. Stripe counts only the first
attempt and its automatic retries, so if the customer retries by hand after a
run has finished and before Stripe retried automatically, that failure still
has `attemptCount` `1` and starts a new run for the invoice. An invoice that
is voided, marked uncollectible, or ends with a cancelled subscription never
sends `saas.purchase`; add a condition step before each email, for example one
that ends the run when the contact has the `churned` tag, if those runs should
end early. With the `attemptCount` filter, an invoice whose first failure
arrived before Stripe was connected does not start a run.
### Revenue Attribution
Sequenzy credits `saas.purchase` revenue to the most recent qualifying email
touch inside your attribution window. First payments count,
including the first paid charge when a trial converts. Renewals from contacts
who are already paying customers do not. See
[which subscription payments count](/concepts/goals#which-subscription-payments-count).
## Manual Sync
To re-sync revenue data at any time:
1. Go to **Settings → Integrations**
2. Find your connected Stripe account
3. Click **Sync Revenue**
The sync processes all Stripe customers and updates matching subscribers. Large customer bases are processed in batches automatically.
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Stripe customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If a Stripe customer doesn't have an email or doesn't match any subscriber, they're skipped during sync.
## Default Segments
When you connect Stripe, Sequenzy automatically creates three segments to help you get started:
* **Customers (Low MRR)**: Subscribers with `mrr < 50` who have the `customer` tag
* **High Spenders**: Subscribers with `ltv > 500`
* **Churned High Spenders**: Subscribers with the `churned` tag and `ltv > 500`
You can customize or delete these segments at any time.
# Supabase Integration
Source: https://docs.sequenzy.com/integrations/supabase
Sync users from your Supabase database with automatic webhooks and manual backfills
Connect your Supabase database to Sequenzy so users can become subscribers. Webhooks keep new database changes in sync automatically, and manual sync lets you backfill or refresh users from a selected table when needed.
## What is Supabase?
[Supabase](https://supabase.com) is an open-source Firebase alternative that provides a PostgreSQL database, authentication, real-time subscriptions, and more. Many SaaS applications use Supabase as their backend.
## Connecting Supabase
### Step 1: Authorize Supabase
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Supabase
3. Click **Connect** in the Supabase account access panel
4. Approve access in Supabase
Sequenzy uses Supabase OAuth with PKCE and stores encrypted access tokens so the integration can use the Supabase Management API.
The OAuth app needs these Supabase scopes:
* `organizations:read`
* `projects:read`
* `database:read`
* `database:write`
* `auth:write`
### Step 2: Configure User Sync
1. Go to **Settings → Integrations** in Sequenzy
2. Open the Supabase integration
3. Choose the Supabase project
4. Enter the schema and table to sync, usually `public.profiles`
5. Choose whether synced users should use your workspace default lists, selected lists, or no lists
6. If you want a Supabase column to become the subscriber's external ID, enter the column name in **External ID column**. For example, use `id` if your row ID is also your application user ID.
7. If your table has a marketing consent flag, enter it in **Subscribed column**. For example, `is_subscribed`.
8. If some subscribed rows require confirmation before marketing email, enter your boolean confirmation column in **Double opt-in column**. For example, `requires_double_opt_in`.
9. Click **Save**
Saving stores the sync settings and configures the Supabase trigger used for automatic webhooks.
### Step 3: Enable Automatic Webhooks
Automatic webhooks keep future inserts, updates, and deletes in sync without pressing **Sync users**.
1. Confirm the project, schema, table, lists, and external ID column
2. Click **Save**
3. If a webhook already exists, saving refreshes the trigger for the selected table
Sequenzy creates a `pg_net` trigger through Supabase OAuth, generates a webhook secret, and stores the encrypted secret in your workspace. Supabase Database Webhooks use this same trigger mechanism under the hood.
If Supabase denies webhook creation, reconnect Supabase after confirming the
OAuth app has `database:write` access.
### Step 4: Run Manual Sync
Manual sync is separate from webhooks. Use it to import existing rows, backfill users after changing list settings, or refresh subscribers from the selected table.
1. Open the Supabase integration modal
2. Confirm the project, schema, table, lists, and external ID column
3. Click **Save** if you changed the settings
4. Close the modal and click **Sync users** on the Supabase integration row
5. Confirm the sync
After this, webhooks still handle future changes automatically when they are configured.
Once the table is saved, the same backfill can be re-run without the dashboard - useful when an agent is preparing a campaign that needs names or plan attributes on users who existed before the trigger was installed:
```bash theme={null}
# CLI
sequenzy integrations sync INTEGRATION_ID
# API
curl -X POST "https://api.sequenzy.com/api/v1/integrations/INTEGRATION_ID/sync" \
-H "Authorization: Bearer YOUR_API_KEY"
```
The MCP tool `sync_integration` does the same thing. All three read the project, schema, and table already saved on the integration, so choosing the table stays a one-time dashboard step.
### Retarget Which Lists Synced Users Join
If the trigger is adding users to lists it should not, change the targeting without reconfiguring the integration:
```bash theme={null}
# Land new contacts on specific lists
sequenzy integrations set-lists INTEGRATION_ID --list LIST_ID
# Or on no list at all
sequenzy integrations set-lists INTEGRATION_ID --none
# Or back to your workspace default lists
sequenzy integrations set-lists INTEGRATION_ID --default
```
The same field is on `PATCH /api/v1/integrations/{id}` as `listIds`, and on the `set_integration_list_targeting` MCP tool. Both need a key with `integrations:manage`. It is the same setting as the lists picker in the Supabase modal, so the dashboard and the API cannot disagree.
This changes list membership only. Users are still created from every INSERT,
their attributes still sync, sync-rule tags still apply, and default Any
contact sequences still enroll them. Any list and specific-list sequences
require a matching membership and do not enroll a list-less contact. Pause the
Any contact sequences as well if that enrollment must stop.
Targeting applies from the next trigger firing onwards and is never applied retroactively, and no subscriber is ever removed from a list. It is not creation-only, though: a row going from unsubscribed back to subscribed re-runs the subscribe path, so that existing contact joins the newly targeted lists. A plain row update goes through profile sync and leaves list membership alone.
Pausing bulk sync (`disable-sync`) does not affect any of this either - the database trigger keeps firing regardless. To stop Supabase feeding the workspace entirely, disconnect it from Settings → Integrations, which also drops the trigger.
## Route Supabase Auth and Security Emails Through Sequenzy SMTP
The database webhook above handles subscriber sync. Supabase Auth emails are a separate path.
If you want Supabase invites, magic links, password resets, and security notifications to send through Sequenzy too, Sequenzy can configure Supabase SMTP and the Supabase Auth email templates through OAuth.
### Step 5: Configure Auth Emails Automatically
1. Open the Supabase integration modal
2. Go to **Auth emails**
3. Choose the Supabase project
4. Click **Configure Auth emails**
Sequenzy creates the default Supabase transactional templates, provisions a sender if needed, generates a dedicated SMTP API key, and updates Supabase Auth with Sequenzy SMTP settings and template payloads.
If Supabase denies the update, reconnect Supabase after adding `auth:write` to
the OAuth app scopes.
### Manual Fallback: Configure Supabase SMTP
If automatic Auth setup is unavailable, create a Sequenzy API key in **Settings → API Keys & SMTP** and configure custom SMTP in Supabase under **Authentication → Emails → SMTP Settings**:
| Setting | Value |
| - | - |
| Host | `smtp.sequenzy.com` |
| Port | `587` |
| Username | `api` |
| Password | Your Sequenzy API key |
Use a sender address that belongs to a verified Sequenzy sending domain. Supabase still needs a sender configured, even when Sequenzy renders the final template.
### Manual Fallback: Point Supabase Email Templates at Sequenzy Transactionals
If your workspace does not have a verified sender profile yet, Sequenzy automatically provisions a shared-domain sender and assigns it to the default Supabase templates.
In Sequenzy, open the Supabase integration modal and click **Create default Supabase auth templates**. This provisions the standard transactional emails for:
* `supabase-confirm-signup`
* `supabase-invite`
* `supabase-magic-link`
* `supabase-password-reset`
* `supabase-email-change-confirmation`
* `supabase-email-change`
* `supabase-reauthentication`
* `supabase-password-changed`
* `supabase-phone-changed`
* `supabase-identity-linked`
* `supabase-identity-unlinked`
* `supabase-mfa-enrolled`
* `supabase-mfa-removed`
Then replace the Supabase message body with a JSON payload that references the matching Sequenzy slug.
You can reference the Sequenzy email by `slug` or `transactionalId`. `variables` and `dataVariables` are both supported.
Example for a Magic Link template:
```json theme={null}
{
"slug": "supabase-magic-link",
"dataVariables": {
"confirmation_url": "{{ .ConfirmationURL }}",
"token": "{{ .Token }}",
"token_hash": "{{ .TokenHash }}",
"redirect_to": "{{ .RedirectTo }}",
"site_url": "{{ .SiteURL }}",
"email": "{{ .Email }}"
}
}
```
Example for an Email Changed notification:
```json theme={null}
{
"slug": "supabase-email-change",
"dataVariables": {
"email": "{{ .OldEmail }}",
"old_email": "{{ .OldEmail }}",
"new_email": "{{ .Email }}"
}
}
```
### Suggested Variable Mapping
| Supabase template variable | Suggested Sequenzy merge tag |
| - | - |
| `{{ .ConfirmationURL }}` | `{{confirmation_url}}` |
| `{{ .Token }}` | `{{token}}` |
| `{{ .TokenHash }}` | `{{token_hash}}` |
| `{{ .RedirectTo }}` | `{{redirect_to}}` |
| `{{ .SiteURL }}` | `{{site_url}}` |
| `{{ .Email }}` | `{{email}}` |
| `{{ .NewEmail }}` | `{{new_email}}` |
| `{{ .OldEmail }}` | `{{old_email}}` |
| `{{ .Phone }}` | `{{phone}}` |
| `{{ .OldPhone }}` | `{{old_phone}}` |
| `{{ .Provider }}` | `{{provider}}` |
| `{{ .FactorType }}` | `{{factor_type}}` |
### Recommended Setup
* Keep the database webhook integration enabled for subscriber sync
* Use Sequenzy SMTP for Supabase Auth email delivery
* Create one Sequenzy transactional email per Supabase template type
* Use clear slugs like `supabase-invite`, `supabase-magic-link`, and `supabase-password-reset`
## What Gets Synced
Sequenzy automatically maps common database columns to subscriber attributes:
### Email Detection
Sequenzy looks for email in these columns (in order):
* `email`
* `email_address`
* `user_email`
* `contact_email`
### User ID Detection
User ID is extracted from:
* `id`
* `user_id`
* `uid`
* `uuid`
The detected user ID is stored as the `supabaseUserId` subscriber attribute. If you set **External ID column** in the Supabase integration settings, that column is also written to the subscriber's external ID and used to merge future webhook updates into the same subscriber.
### Synced Attributes
| Database Column | Subscriber Attribute |
| - | - |
| `first_name` | `firstName` |
| `last_name` | `lastName` |
| `full_name` | `fullName` |
| `avatar_url` | `avatarUrl` |
| `phone` | `phone` |
| Any other column | Stored as custom attribute |
Sensitive columns like `password`, `password_hash`, and `encrypted_password`
are automatically excluded.
### Consent Mapping
Supabase sync can use a top-level row column to decide whether someone is currently subscribed. Configure it in **Settings → Integrations → Supabase → Advanced options → Subscribed column**.
When the configured subscribed column is truthy (`true`, `1`, `yes`, or `subscribed`), Sequenzy syncs the row through the normal subscribed path. When it is falsey (`false`, `0`, `no`, or `unsubscribed`), Sequenzy keeps the subscriber unsubscribed and out of lists. If the column is missing or empty, Sequenzy falls back to the normal sync behavior.
If your table uses custom status values, add **Truthy values** for the exact values that should count as subscribed. For example, if your source column is named `status` and the value `unsubscribed` means "sync this row", add `unsubscribed` as the subscribed truthy value.
### Double Opt-In Mapping
Supabase sync can use a top-level row column to decide consent per subscriber. Configure the column in **Settings → Integrations → Supabase → Advanced options → Double opt-in column**.
When the configured column is truthy (`true`, `1`, `yes`, or `required`), Sequenzy creates or updates the subscriber as unconfirmed, sends your workspace double opt-in email, and waits to add the subscriber to lists until they click the confirmation link.
When the configured column is falsey (`false`, `0`, `no`, or `confirmed`), Sequenzy treats the row as already confirmed consent and syncs it as an active subscriber. If the column is missing or empty, Sequenzy falls back to your normal workspace double opt-in setting.
If your table uses custom values, add **Truthy values** for the exact values that should require confirmation. Any other non-empty value is treated as already confirmed.
Before using a double opt-in column, configure your workspace confirmation
email in **Settings → Email tracking → Double opt-in**.
## Tracked Events
| Database Event | Sequenzy Event |
| - | - |
| `INSERT` | `contact.subscribed` |
| `UPDATE` | Attributes updated (no event) |
| `DELETE` | `contact.unsubscribed` |
## Using with Automations
### Welcome Sequence for New Users
1. Go to **Automations** → **Create New**
2. Set trigger to **Event Received** → `contact.subscribed`
3. Add your welcome email sequence
4. Activate the automation
Every new row in your users table will trigger the welcome sequence.
### Custom Attributes
Any column in your users table becomes available for segmentation and personalization:
```sql theme={null}
-- Your users table might have:
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT,
first_name TEXT,
plan TEXT, -- becomes {{ plan }}
company_name TEXT, -- becomes {{ company_name }}
created_at TIMESTAMP
);
```
Use these in emails:
```
Hi {{firstName}},
Thanks for signing up for the {{plan}} plan!
```
## Database Schema Recommendations
For best results, include these columns in your users table:
```sql theme={null}
-- Recommended columns
email TEXT NOT NULL, -- Required for subscriber creation
first_name TEXT, -- Maps to firstName
last_name TEXT, -- Maps to lastName
-- Optional but useful
avatar_url TEXT, -- Profile picture
phone TEXT, -- Phone number
plan TEXT, -- Subscription plan
created_at TIMESTAMP -- For tracking
```
## User Lifecycle
| Supabase Event | Sequenzy Action |
| - | - |
| Row inserted | Subscriber created, `contact.subscribed` triggered |
| Row updated | Subscriber attributes updated |
| Row deleted | Subscriber unsubscribed |
When you delete a user from your database, they're automatically unsubscribed in Sequenzy.
### Deleting and Re-creating Users
A deletion-driven unsubscribe is reversible by the same integration: if the user signs up again (a new `INSERT` for the same email, or a row that reappears in a manual sync), Sequenzy reactivates the contact automatically. This keeps test loops working - deleting a test user and re-inviting them does not leave the contact permanently unsubscribed.
Explicit opt-outs are never undone this way. If the contact unsubscribed through an email link, the dashboard, or the API, re-creating their user in Supabase does not resubscribe them.
While a contact is unsubscribed, events they trigger still appear in Recent Activity, but matching sequences show "Would trigger if subscribed" instead of enrolling them, and the sequence's trigger step shows a skipped count.
## Troubleshooting
### Users Not Syncing
1. Verify the webhook URL is correct in Supabase
2. Check that the `x-webhook-secret` header matches the secret in Sequenzy
3. Ensure all events (INSERT, UPDATE, DELETE) are selected
4. Check Supabase's webhook logs for failed deliveries
### Missing Email
If users are inserted without an email column value, Sequenzy cannot create a subscriber. Ensure your users table has a valid email.
### Wrong Columns Mapping
Sequenzy looks for standard column names. If your schema uses different names:
* `email_address` → works (detected automatically)
* `customer_email` → works (detected automatically)
* `e_mail` → won't be detected as email
Consider using standard column names or storing data in the expected format.
### Signature Verification Failed
1. Verify the `x-webhook-secret` header value in Supabase matches exactly what you entered in Sequenzy
2. Secrets are case-sensitive
3. Try generating a new secret and updating both Supabase and Sequenzy
## FAQ
Use whichever table contains your user data. This is typically `users`, `profiles`, or `auth.users`. If you're using Supabase Auth, you might want to create a trigger to sync to a `profiles` table with the columns you need.
Yes. Click **Sync users** in the Supabase integration modal to manually import
existing rows from the selected table, or re-run the same backfill with
`sequenzy integrations sync `, `POST /v1/integrations/{id}/sync`, or the
`sync_integration` MCP tool. Webhooks handle future changes after they are
configured.
You can configure webhooks for additional tables in Supabase. Manual sync uses
the table currently saved in the integration modal.
Yes, Supabase integration is included at no extra cost.
# Warehouse Sync
Source: https://docs.sequenzy.com/integrations/warehouse-sync
Import contacts and events from Snowflake, BigQuery, Redshift or Postgres on a schedule, sending only rows that changed.
Warehouse Sync keeps Sequenzy in step with your data warehouse. You write a SQL query, map its columns, and Sequenzy runs it on a schedule: contacts are created or updated, and events are recorded on your contacts. Each run sends only rows that changed since the last run.
Set it up in **Settings → Data Warehouse**, or with the [API](/api-reference/warehouse/connections-create), the [CLI](/concepts/cli) (`sequenzy warehouse`) or the [MCP server](/concepts/mcp).
Sending data the other way? [Data exports](/integrations/data-exports) stream
email events and subscriber snapshots to your bucket for loading into the same
warehouse.
## 1. Create a read-only user
Sequenzy only runs `SELECT` queries, and on Postgres and Redshift every query runs in a read-only transaction. Still, give it a user that can only read what you want to sync.
Sequenzy uses key-pair authentication. Generate a key and assign its public half to a new user:
```bash theme={null}
openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out sequenzy_key.p8 -nocrypt
openssl rsa -in sequenzy_key.p8 -pubout -out sequenzy_key.pub
```
```sql theme={null}
CREATE ROLE SEQUENZY_READ;
GRANT USAGE ON WAREHOUSE ANALYTICS_WH TO ROLE SEQUENZY_READ;
GRANT USAGE ON DATABASE PROD TO ROLE SEQUENZY_READ;
GRANT USAGE ON SCHEMA PROD.PUBLIC TO ROLE SEQUENZY_READ;
GRANT SELECT ON ALL TABLES IN SCHEMA PROD.PUBLIC TO ROLE SEQUENZY_READ;
GRANT SELECT ON ALL VIEWS IN SCHEMA PROD.PUBLIC TO ROLE SEQUENZY_READ;
CREATE USER SEQUENZY_READER TYPE = SERVICE DEFAULT_ROLE = SEQUENZY_READ
RSA_PUBLIC_KEY = 'MIIBIjANBgkq...'; -- contents of sequenzy_key.pub without the header lines
GRANT ROLE SEQUENZY_READ TO USER SEQUENZY_READER;
```
Then connect with your account identifier (for example `myorg-myaccount`), the user, warehouse, database, and the contents of `sequenzy_key.p8`.
Create a service account, then grant it:
* **BigQuery Job User** on the project that runs the queries.
* **BigQuery Data Viewer** on the datasets you query.
Create a JSON key for the service account and paste its contents when you connect. Sequenzy shows the service account email on the connection so you can see whom to grant.
```sql theme={null}
CREATE USER sequenzy_reader PASSWORD '...';
GRANT USAGE ON SCHEMA analytics TO sequenzy_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO sequenzy_reader;
```
The database must accept TLS connections from the public internet. Use `verify-full` to also check the server certificate. Private networks, SSH tunnels and IP allowlists are not supported yet.
When you save a connection, Sequenzy runs a test query. If it fails, nothing is saved and you see the warehouse's reason. Credentials are stored encrypted and never shown again.
## 2. Write the query
A sync runs one `SELECT` (or `WITH ... SELECT`) statement. Sequenzy wraps it as a subquery, so any query your warehouse accepts works, including joins and views. The query cannot contain semicolons, even inside strings; use `CHR(59)` (`CODE_POINTS_TO_STRING([59])` on BigQuery) if you need to match one:
```sql theme={null}
select
u.email,
u.id as user_id,
u.first_name,
a.plan,
a.mrr,
u.updated_at
from analytics.users u
join analytics.accounts a on a.id = u.account_id
where u.email is not null
```
Use **Preview** to run the query with a 20-row limit and see its columns before you map them. Column names are matched exactly as your warehouse returns them; Snowflake returns unquoted names in upper case.
## 3. Map columns
| Field | What it does |
| - | - |
| Email | Required (or a phone column for SMS-only contacts). Identifies the contact. |
| External ID | Your own user ID, stored on the contact. |
| First name, last name | Contact names. |
| Tags | An array column, or a comma-separated string. Tags are added, never removed. |
| Custom attributes | Any other columns, each with an attribute name. Numbers, booleans, dates and arrays keep their types. |
Contacts go through the same pipeline as [imports](/api-reference/subscribers/import-create), with suppression checks and risk review. For existing contacts, mapped values replace the stored ones, empty values never clear what is already there, and unsubscribed contacts are never resubscribed. Contacts the sync creates join the lists you choose; existing contacts keep their list memberships.
Imports run as the person who last saved the sync. If that person leaves the workspace or can no longer edit contacts, runs stop with a message; open the sync and save it to continue as you.
| Field | What it does |
| - | - |
| Contact email or external ID | Finds the contact. Events for contacts that do not exist yet are skipped. With a cursor column they are read again only when the row changes or on a full resync, so sync contacts first. |
| Event name | A column, or one fixed name for every row. |
| Event ID | A stable ID per event, such as an order ID. When omitted, one is derived from the row. An event is recorded once per ID: later changes to its properties are not applied. |
| Occurred at | When the event happened. Defaults to the time of the sync. |
| Properties | Any other columns, each with a property name. |
Events are deduplicated by ID, so re-reading a row never records it twice. Events more than an hour old are stored as history: they count in segments and reports but never start automations.
## 4. Choose how it runs
* **Cursor column.** Pick a column that grows whenever a row changes, such as `updated_at`. Each run then reads only rows at or after the last value it saw, which keeps large tables cheap. Rows with an empty cursor are not read. Without a cursor, every run reads the whole result (up to 2,000,000 rows) and skips unchanged rows. Your warehouse bills each run as it bills any query; on BigQuery, a cursor on a partitioned or clustered column keeps the bytes scanned small.
* **Schedule.** Every 15 minutes, hourly, daily, weekly, or manual only. The first run starts when you create the sync.
* **Start automations.** Off by default. When on, new contacts can enter sequences that start on contact creation, and events from the last hour can start or advance sequences.
* **Double opt-in.** Off by default, for warehouses that only hold contacts who already agreed to receive email. When on, new contacts get a confirmation email first.
## How runs work
* **Only changes are sent.** Sequenzy keeps a fingerprint of every row it sent. Unchanged rows are counted as unchanged and skipped. A row is only recorded as sent after the import or event write accepted it. Rows that failed inside Sequenzy are read again on the next run; a row that keeps failing where the last run stopped is skipped with a note so the sync can move on. If an import later loses part of its rows, the next run sends every row again, up to three times a day.
* **Long backfills continue on their own.** With a cursor, a run stops after 30 minutes and the next one starts right away from where it stopped. Without a cursor, the rest waits for the next scheduled run. More than 2,000,000 rows with the same cursor value stop the sync with an error; use a more precise cursor column.
* **Edits apply right away.** Pausing, deleting or changing a sync stops a run in progress at its next page of rows; a changed sync then runs again with the new settings.
* **Progress.** The dashboard, `sequenzy warehouse syncs runs` and the API show rows read, synced, unchanged, skipped and failed, sample problems with their row numbers, and the progress of the contact imports each run queued.
* **Failures retry.** When a run fails, for example because of an expired key or a SQL error, the sync shows the reason and retries with backoff. Fix the problem and choose **Run now** to retry immediately.
* **Full resync.** Forgets what was sent and the cursor position, then sends every row again. Use it after changing data without touching the cursor column.
## Permissions
Only owners and admins can manage warehouse syncs in the dashboard. API keys need:
| Action | Scopes |
| - | - |
| View connections, syncs and runs | `warehouse:read` |
| Create, change, test or preview connections | `warehouse:write` |
| Create, change or run syncs | `warehouse:write` and `subscribers:write` |
| Event syncs | also `events:write` |
| Start automations or send double opt-in emails | also `automations:trigger` |
| Change the query, mapping, cursor or lists of a sync that starts automations or sends double opt-in emails, or fully resync it | also `automations:trigger` |
| Delete connections or syncs | `warehouse:delete` |
# Webflow Forms
Source: https://docs.sequenzy.com/integrations/webflow
Capture explicitly selected native Webflow forms with field-level controls and consent-aware enrollment.
Sequenzy is a Webflow Data Client integration. It reads native form schemas
through Webflow's Forms API and receives selected form submissions through
signed, form-filtered Webflow webhooks. It does not ask you to paste HTML,
React, JavaScript, custom code, or a direct-to-Sequenzy form action into
Webflow.
OAuth authorization does not start capture. Sequenzy fails closed until an
administrator explicitly selects a native form, its allowed fields, a consent
policy, and enrollment settings.
## Connect And Configure
1. Install Sequenzy from the Webflow Marketplace, or open **Settings →
Integrations → Websites & Forms → Webflow → Connect**.
2. Review the requested scopes in Webflow. Choose only the sites or workspace
Sequenzy should be authorized to read, then approve the connection.
3. Back in Sequenzy, open **Webflow → Select forms**.
4. Select each native form Sequenzy may process. Unselected forms are excluded.
5. For each selected form:
* confirm the native **Email** field;
* allow only the additional fields Sequenzy needs;
* review the required Sequenzy double opt-in policy.
6. Choose explicit list enrollment. **No lists** is the default.
7. Leave **Run eligible sequences** off unless submissions should start
configured automations.
8. Save. Sequenzy creates one `form_submission` webhook filtered to each
selected form name.
Only workspace owners and admins can connect or reconfigure Webflow. Existing
submissions are not imported.
## Form And Field Selection
Sequenzy uses `GET /v2/sites/{siteId}/forms` to show the native forms returned
by Webflow. A form is processed only when its Webflow form ID and site ID match
a saved capture rule.
* Unselected sites and forms are rejected.
* Removing a selected form reconciles and removes its Sequenzy-owned webhook.
* Clearing every selection turns capture off and removes the integration's
webhooks.
* Duplicate form names on one site cannot be selected because Webflow's webhook
filter is name-based. Rename the forms in Webflow, then use **Refresh from
Webflow** to load the current schema.
* Forms without a native Email field cannot be selected.
* Password, passcode, credential, authentication-token, and secret fields are
never selectable.
* Use **Refresh from Webflow** after publishing or changing a native form.
Refresh removes every saved selection and webhook, then leaves capture off
until an administrator reviews the new schema and saves exact rules again.
It also clears destination lists and turns off sequence enrollment, so review
those settings before saving. The form-selection screen shows **Schema last
fetched** beside the refresh control and flags forms with duplicate names.
Webflow sends the selected form's submission to the webhook. After signature
verification, Sequenzy immediately applies your field allowlist. Excluded
fields are discarded before a background job is queued and are not stored by
Sequenzy.
## Consent And Enrollment
A Webflow submission is not marketing consent by itself. Every selected form
uses **Sequenzy double opt-in**. A valid submission creates a pending contact
and queues the confirmation email; the contact cannot receive marketing email
until they confirm. Sequenzy does not infer consent from a Webflow submission
or from an undocumented field type.
List enrollment and sequence enrollment are separate controls:
* New connections default to **No lists**.
* Selecting one or more lists adds accepted contacts only to those lists.
* **Run eligible sequences** defaults to off.
* Event automations do not run while the contact is pending. Eligible
contact/list automations run only after confirmation and only when **Run
eligible sequences** is enabled.
Changing these controls affects future submissions. It does not remove an
existing contact or list membership.
## Data Flow And Storage
Form schemas are snapshots from Webflow as of **Schema last fetched**. Captured
submissions are historical copies of the allowed fields at capture time, not a
live view of Webflow. Consent status, list membership, and sequence enrollment
are managed in Sequenzy. Consent metadata on a submission event describes that
event, not the subscriber's current consent status.
Later changes or deletions of submissions, forms, or sites in Webflow do not
automatically update or delete subscribers or events already captured in
Sequenzy. Disconnecting stops capture but does not delete those records.
Delete subscribers in Sequenzy; for retained event deletion, use the workspace
deletion process or contact `privacy@sequenzy.com`. The five-year event retention
period is fixed and is not configurable per workspace.
| Stage | Data handled | Retention and behavior |
| - | - | - |
| OAuth | Encrypted access token; authorization, user, site, native-form schema, and customer-owned custom-domain metadata | Kept while connected. Used only for the Webflow integration and deleted after successful disconnect cleanup. |
| Webhook verification | Raw request body, `x-webflow-signature`, and `x-webflow-timestamp` | Used in request memory to verify the HMAC and five-minute timestamp window. Invalid, expired, future-dated, and replayed deliveries are rejected or safely acknowledged without processing. |
| Consent and filtering | Selected form metadata and configured field allowlist | Applied synchronously. Unconfigured forms, excluded fields, invalid configured emails, and sensitive/password fields do not enter the job queue. Accepted contacts remain pending until double opt-in confirmation. |
| Background job | Only selected field values plus site, form, submission, and submitted-at identifiers | Successful jobs are removed within 24 hours; failed jobs within 7 days, with count limits as an additional bound. |
| Subscriber | Email, detected first/last name when allowed, consent metadata, and selected list membership | Kept until the subscriber or workspace is deleted, subject to the privacy policy and legal obligations. |
| `webflow.form_submission` event | Site/form/submission metadata and the allowed fields under `formData` | Stored in Sequenzy's subscriber-event store for up to five years, or deleted earlier with the workspace/deletion process. |
| Replay keys | A hash derived from delivery metadata and the Webflow submission ID | Delivery keys expire after 10 minutes. Processed submission IDs expire after 30 days. The raw signature is not stored in the key. |
Sequenzy does not publish or modify Webflow pages, Designer elements, form
markup, CMS content, assets, custom code, or site styles. The integration adds
no fonts, images, third-party asset requests, or “Powered by Sequenzy” link to
the published site. While Webflow is connected, Sequenzy also disables its
custom form and popup embed code and rejects submissions to previously deployed
Sequenzy embed endpoints for that workspace. This policy is enforced by the
public API, not only hidden in the dashboard.
## Event Shape
Accepted submissions record `webflow.form_submission`. Only fields selected in
the allowlist appear under `formData`:
```json theme={null}
{
"provider": "webflow",
"webflowTriggerType": "form_submission",
"webflowSiteId": "65427cf400e02b306eaa049c",
"webflowFormId": "65429eadebe8a9f3a30f62d0",
"webflowFormName": "Newsletter",
"webflowSubmissionId": "6321ca84df3949bfc6752327",
"webflowSubmittedAt": "2026-08-20T12:35:16.117Z",
"webflowConsentMode": "double_opt_in",
"webflowConsentStatus": "pending_double_opt_in",
"webflowConsentRequestedAt": "2026-08-20T12:35:16.117Z",
"formData": {
"Email": "ari@example.com",
"First Name": "Ari"
}
}
```
## Security And Ownership
For every delivery, Sequenzy:
1. verifies `x-webflow-signature` over the timestamp and exact raw body using
the Webflow app secret;
2. rejects timestamps older than five minutes or more than 30 seconds in the
future;
3. claims short-lived delivery and submission replay keys before queueing;
4. requires an active local integration and an exact saved form/site rule;
5. re-introspects the OAuth token in the worker and verifies the site or
workspace is still authorized immediately before any subscriber write;
6. uses a stable Webflow submission event ID so worker retries cannot create a
second event.
If Webflow authorization is revoked or the site is no longer authorized, the
worker deactivates the integration and stops processing before data mutation.
For a still-valid but narrowed authorization, it queues the same cleanup used
by an explicit disconnect. For a revoked token, it deletes the credential
immediately, keeps the incomplete cleanup visible, and shows the manual path
below because the Webflow API can no longer be called.
## Custom Domains And Sending Domains
Sequenzy reads customer-owned domains from Webflow's Custom Domains API for
site context and domain suggestions. It never suggests a `*.webflow.io` staging
domain as a sending domain. You can still enter another domain manually, but
you must own and verify it before sending.
Your workspace may initially use a Sequenzy default sending domain. You can use
it for confirmation emails, but neither a default domain nor a verified custom
domain guarantees inbox placement. **Sent** does not prove that Gmail placed a
message in the inbox. A confirmation email in spam can prevent a new contact
from completing double opt-in.
For your own sending identity, you can open **Settings → Domain**, configure a
sending domain you own, complete the DNS verification instructions, and select
a sender profile on that domain as your workspace default. A custom domain is
optional. The Webflow form-selection
screen displays this guidance even when you already have a custom domain.
Submit a test form and complete confirmation to verify the full flow. If a
confirmation email lands in spam, inspect its authentication results and
contact support with the sending address and message details. Do not assume
that changing domains alone fixes deliverability.
## Disconnect And Cleanup
When an administrator clicks **Disconnect**, Sequenzy immediately marks the
integration inactive. New webhook requests stop processing. A cleanup job then:
1. deletes only webhook IDs recorded by Sequenzy and matching this workspace's
callback URL;
2. lists Webflow app-owned webhooks as a recovery check and deletes only the
same matching callback registrations;
3. revokes the OAuth authorization when no other active Sequenzy workspace uses
that grant;
4. deletes the encrypted token, Webflow user/site/form metadata, capture rules,
and local webhook registrations.
Workspace deletion is blocked while a Webflow token or cleanup is still
present. Disconnect Webflow and wait for cleanup before deleting the workspace.
The Integrations page displays any pending or failed cleanup and provides a
**Retry cleanup** action. Workspace deletion also synchronously removes the
workspace's retained ClickHouse events, attributes, audience rows, and
analytics aggregates.
If the OAuth grant was removed in Webflow before Sequenzy could delete its
webhooks, automatic cleanup may be impossible because the API token no longer
works. Reconnect Webflow to the same sites and disconnect again, or remove the
Sequenzy app/remaining Sequenzy webhook registrations in Webflow. If either
path is unavailable, contact `privacy@sequenzy.com` with the Sequenzy workspace
ID and Webflow site ID for an assisted deletion check.
## Verify The Complete Flow
1. Confirm the integration says **capture off** immediately after OAuth.
2. Select one native form and exclude at least one non-required field.
3. Review double opt-in, configure lists and sequences, then save.
4. Submit an unselected form. Confirm no Sequenzy contact or event appears.
5. Submit the selected form with a unique email.
6. Verify the contact is pending and no marketing sequence runs before email
confirmation.
7. Confirm the email, then verify the subscriber, consent metadata, exact list membership, and
`webflow.form_submission` event. Confirm the excluded field is absent.
8. Disconnect and confirm webhook cleanup completes; verify the app grant is
removed when it is no longer shared.
## Permissions
| Scope | Use |
| - | - |
| `authorized_user:read` | Shows the identity that approved the connection. |
| `forms:read` | Lists native forms and field schemas for explicit selection. |
| `sites:read` | Lists authorized sites, custom domains, and app-owned webhook registrations; permits deletion of those app-owned webhooks. |
| `sites:write` | Creates Sequenzy's form-filtered webhooks. |
## Related Guides
* [Privacy Policy](https://www.sequenzy.com/privacy)
* [Webflow Forms API](https://developers.webflow.com/data/reference/forms/forms/list)
* [Webflow Create Webhook API](https://developers.webflow.com/data/reference/webhooks/create)
* [Webflow Custom Domains API](https://developers.webflow.com/data/reference/sites/get-custom-domain)
# Whop Integration
Source: https://docs.sequenzy.com/integrations/whop
Sync revenue data from Whop to power segments and automations
Connect your Whop account to automatically sync revenue metrics for all your subscribers. This enables powerful segmentation based on MRR and LTV.
## What is Whop?
[Whop](https://whop.com) is a marketplace and commerce platform for selling access to communities, software, courses, and other digital products. Creators list memberships and products on Whop, and Whop handles checkout, billing, and access management for their customers.
## Connecting Whop
Whop requires manual API key and webhook setup. Connecting happens in two steps in Sequenzy: first you provide your Company ID and API key, then you set up a webhook and provide its signing secret.
### Step 1: Find Your Whop Company ID
1. Go to your [Whop Dashboard](https://whop.com/dashboard)
2. Your **Company ID** is visible in the dashboard URL (format: `biz_xxxxxxxxx`)
3. Copy this ID for later
### Step 2: Create a Whop API Key
1. In the Whop Dashboard, go to the **Developer** tab
2. Open **Account API Keys**
3. Click **Create**
4. Copy the API key
Your API key must include the `member:basic:read`, `member:email:read`,
`payment:basic:read`, `plan:basic:read`, `access_pass:basic:read`,
`member:phone:read`, `promo_code:basic:read`, `payment:dispute:read`, and
`payment:resolution_center_case:read` permissions. Whop requires this complete
permission set for payment history and authoritative refund totals. Without
`member:email:read`, Whop omits customer emails and Sequenzy cannot match
payments to your subscribers. Make sure all nine permissions are enabled
before creating the key.
### Step 3: Enter Your Company ID and API Key in Sequenzy
1. Go to **Settings → Integrations** in Sequenzy
2. Click **Connect** next to Whop
3. Enter your:
* **Company ID** (from Step 1)
* **API Key** (from Step 2)
4. Continue to the next step
### Step 4: Set Up Webhooks in Whop
1. In the Whop Dashboard, go to the **Developer** tab
2. Click **Create Webhook**
3. Enter your Sequenzy webhook URL:
```
https://api.sequenzy.com/api/webhooks/whop/{your-sequenzy-company-id}
```
This path uses your Sequenzy company ID, not the Whop `biz_...` Company ID
from Step 1. The connect modal shows the correct URL already filled in,
with a copy button, so copy it from there instead of building it by hand.
4. Select these events to listen for:
* `payment.succeeded`
* `payment.failed`
* `membership.activated`
* `membership.deactivated`
* `refund.created`
* `refund.updated`
5. Click **Create** to save the webhook
6. Copy the **Webhook Secret** (format: `whsec_xxxxxxxxx`) shown at creation
### Step 5: Add the Webhook Secret in Sequenzy
1. Back in the Sequenzy connect modal, paste the **Webhook Secret** (from Step 4)
2. Click **Connect Whop**
Sequenzy will immediately begin receiving webhook events from Whop and start a historical sync of your existing customers.
## Synced Attributes
When connected, Sequenzy adds these attributes to each subscriber matched by email:
| Attribute | Type | Description |
| - | - | - |
| `mrr` | Number | Monthly Recurring Revenue in dollars (normalized from any billing interval) |
| `ltv` | Number | Lifetime Value - total payments minus refunds |
| `whopCustomerId` | String | The subscriber's customer ID in Whop |
### MRR Calculation
MRR is calculated from active, trialing, and `past_due` memberships on renewal
plans:
* **Monthly plans**: Used as-is
* **Yearly plans**: Divided by 12
* All amounts are converted to USD
Whop keeps `past_due` memberships eligible for payment retries, so Sequenzy
continues counting their MRR and customer/interval tags until Whop ends access.
One-time plans and completed one-time memberships contribute to LTV, not MRR.
If a plan uses a currency for which Sequenzy has no reliable USD exchange rate,
that membership's MRR is skipped without blocking the rest of the historical
sync. LTV still uses Whop's USD valuation when Whop supplies one.
## Tracked Events
When you connect Whop, these events are automatically tracked for use in automations:
| Event | Triggered When |
| - | - |
| `saas.purchase` | Payment succeeded |
| `saas.purchase.monthly` | Recurring payment on a monthly plan |
| `saas.purchase.yearly` | Recurring payment on a yearly plan |
| `saas.payment_failed` | Payment failed |
| `saas.cancelled` | Membership cancelled |
| `saas.churn` | Membership expired or deactivated |
| `saas.refund` | Refund succeeded |
## Historical Sync
Connecting Whop triggers an automatic backfill of your existing Whop customers and memberships, so your subscribers' `mrr`, `ltv`, and `whopCustomerId` attributes are populated right away rather than only updating as new events arrive.
To re-sync at any time:
1. Go to **Settings → Integrations**
2. Find your connected Whop account
3. Click **Sync Revenue**
## Using Revenue Data
### Building Segments
With MRR and LTV synced as numeric attributes, you can create sophisticated segments:
**Revenue-based segments:**
* `mrr >= 100` - High MRR customers
* `ltv > 1000` - Lifetime high-value customers
* `mrr >= 50 AND mrr < 200` - Mid-tier customers
**Combining with events:**
* Subscribers with a recent `saas.churn` event and high `ltv` - win-back targets
* Subscribers with a recent `saas.cancelled` event and high `mrr` - at-risk high-value customers
### Automations
Trigger automations when revenue events or attributes change:
* Send a thank-you email on `saas.purchase`
* Trigger a win-back sequence when a subscriber gets a `saas.churn` event
* Notify your team when a high-value customer cancels
## How Matching Works
Subscribers are matched by email address (case-insensitive). A Whop customer with email `User@Example.com` will match a subscriber with email `user@example.com`.
If a Whop customer doesn't have an email in the webhook or doesn't match any subscriber, the event is acknowledged but no action is taken.
## Troubleshooting
### Customer Emails Missing
If events are being acknowledged but subscribers aren't being updated:
1. Confirm your API key has the `member:basic:read`, `member:email:read`, `payment:basic:read`, `plan:basic:read`, `access_pass:basic:read`, `member:phone:read`, `promo_code:basic:read`, `payment:dispute:read`, and `payment:resolution_center_case:read` permissions enabled in Whop's Developer tab
2. If a permission was missing when you first connected, create a new API key with all nine permissions, disconnect Whop in Sequenzy, and reconnect it with the new key
3. Check that customers have valid email addresses in Whop
### Webhooks Not Arriving
1. Verify your webhook URL is correct in Whop and includes your Sequenzy company ID
2. Confirm the webhook is enabled and subscribed to `payment.succeeded`, `payment.failed`, `membership.activated`, `membership.deactivated`, `refund.created`, and `refund.updated`
3. Check Whop's webhook logs for failed delivery attempts
### Invalid Webhook Signature
If webhooks are failing with signature errors:
1. Re-copy the `whsec_` secret from Whop's Developer tab, disconnect Whop in Sequenzy, and reconnect it with that secret
2. If you recreated the webhook in Whop, a new secret is generated - reconnect Sequenzy with the latest one
3. Ensure you copied the complete secret, with no extra whitespace
# Wix Integration
Source: https://docs.sequenzy.com/integrations/wix
Connect Wix to capture forms, contacts, and store order events from a Wix app install.
You can connect a Wix site to Sequenzy from **Settings → Integrations**.
The Wix integration is App Market first. After the app is installed, Sequenzy stores the Wix app instance, verifies Wix webhooks, and routes Wix form, contact, and order activity into subscribers and automation events. You do not need Zapier, Make, embed code, or Velo scripts.
## What Gets Synced
* Wix form submissions create or update subscribers and trigger `wix.form_submitted`
* Wix contacts create or update subscribers and trigger `wix.contact_created`
* Wix Stores order creation creates or updates the shopper and triggers `ecommerce.order_placed`
## Setup
Open **Settings → Integrations**, find **Wix** under **E-Commerce**, and
click **Install for Wix**.
Install the Sequenzy app from the Wix App Market on the site you want to
connect.
Connect the Wix app to the right Sequenzy workspace.
Open the Wix gear icon in Sequenzy to choose which lists receive Wix
subscribers.
The integration card shows the connected site and the controls for refreshing,
configuring, or disconnecting it, so you can confirm you linked the intended
workspace before testing.
## List Routing
By default, new Wix subscribers use your workspace default subscriber lists.
Open the Wix settings gear in **Settings → Integrations** and choose:
* Default workspace lists
* No list membership
* Specific lists
This applies to new subscribers created by Wix webhook processing. Existing subscribers are merged with the Wix profile and still receive the Wix automation event.
## Troubleshooting
If Wix activity is not reaching Sequenzy:
* Confirm the Sequenzy app is still installed on the Wix site and the
integration card shows it as connected to this workspace.
* Check that the form, contact, or order includes an email address. Events
without an email are skipped.
* Wix events are processed in the background, so new subscribers can take a
few seconds to appear.
## Notes
* The Wix app install flow is the primary setup path.
* Embedded Sequenzy widgets and direct API calls remain available for custom
Wix builds.
# Zapier Integration
Source: https://docs.sequenzy.com/integrations/zapier
Connect Sequenzy to 6000+ apps with automated workflows
Connect Sequenzy to over 6000 apps using Zapier. Automate your email marketing workflows by connecting to CRMs, forms, payment providers, and more.
## What is Zapier?
[Zapier](https://zapier.com) is a no-code automation platform that connects different apps together. With the Sequenzy integration, you can start Zaps from new subscribers and completed campaigns, or add subscribers, apply tags, and trigger events from other apps.
## Getting Started
1. Go to [Zapier's Sequenzy integration page](https://zapier.com/apps/sequenzy/integrations)
2. Click **Create Zap** or choose a template
3. Follow the prompts to authenticate your Sequenzy account
4. Configure your workflow
## Authentication
When connecting Sequenzy to Zapier, you'll need your API key:
1. Go to **Settings → API Keys & SMTP** in Sequenzy
2. Create a new API key or copy an existing one (current keys start with `seq_live_` for a workspace or `seq_user_` for an account; legacy `ek_` keys are also supported)
3. Paste it in Zapier when prompted
Keep your API key secure. Anyone with this key can manage your subscribers.
### Required API key scopes
Zapier uses one API key for the connection, so give it the union of the scopes needed by the steps in your Zaps:
| Capability | Required scopes |
| - | - |
| Connect the account | `account:read` |
| New Subscriber trigger | `webhooks:write`, `webhooks:delete`, `subscribers:read` |
| Campaign Sent trigger | `webhooks:write`, `webhooks:delete`, `campaigns:read` |
| Find Subscriber search | `subscribers:read` |
| Create or Update, Add Tag, Remove Tag | `subscribers:write` |
| Trigger Event | `subscribers:write`, `events:write`; add `automations:trigger` to start matching automations |
| Delete Subscriber | `subscribers:delete` |
For a connection that uses both triggers and every action, select `account:read`, `webhooks:write`, `webhooks:delete`, `subscribers:read`, `subscribers:write`, `subscribers:delete`, `campaigns:read`, `events:write`, and `automations:trigger`.
## Available Triggers
Both triggers are instant REST hooks. Turning on a Zap creates a dedicated Sequenzy outbound-webhook endpoint; turning it off permanently deletes that endpoint and its delivery history.
### New Subscriber
Starts a Zap when a subscriber is created. Every creation source qualifies, including forms, the API, imports, and integration syncs. With double opt-in enabled, the trigger waits until the contact confirms; an unconfirmed contact does not trigger it.
The record includes subscriber identity and profile fields, custom attributes, creation source, and the list IDs joined during creation.
### Campaign Sent
Starts one Zap when an **email** campaign reaches `sent`. This is an aggregate completion event, not one task per recipient. Empty-audience campaigns and completed A/B tests qualify; SMS campaigns do not. The record includes `sentAt`, `totalSent`, and `totalFailed` from the terminal campaign state.
Sequenzy signs every delivery over the exact raw body with HMAC-SHA256. The Zapier app checks every `v1=` signature, requires a timestamp within five minutes, and rejects missing, stale, malformed, wrong-event, or invalidly signed requests. A rejected or otherwise failed delivery follows Sequenzy's existing policy of up to 16 attempts with exponential backoff spanning 7 days.
## Available Actions
Sequenzy exports exactly five actions for sending data to Sequenzy: Create or Update Subscriber, Add Tag to Subscriber, Remove Tag from Subscriber, Trigger Event for Subscriber, and Delete Subscriber.
For subscriber identity fields, provide **Email** or **External ID**. Use Email when the action may need to create a new subscriber. Use External ID when you already store your app's user/customer ID on the Sequenzy subscriber.
### Create or Update Subscriber
Add a new subscriber by email or update an existing one by email or external ID.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Subscriber's email address. Required when creating a new subscriber. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Can identify an existing subscriber. |
| First Name | No | Subscriber's first name |
| Last Name | No | Subscriber's last name |
| Tags | No | Tags to assign |
| Custom Attributes | No | Key-value pairs for segmentation |
| Enroll in Sequences | No | Whether to enroll in matching sequences |
**Example use cases:**
* Add new leads from Typeform to Sequenzy
* Sync customers from Stripe to Sequenzy
* Import contacts from Google Sheets
### Add Tag to Subscriber
Apply a tag to a subscriber. Creates the subscriber when an email address is provided.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Subscriber's email address. Required when creating a new subscriber. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Can identify an existing subscriber. |
| Tag | Yes | Tag name to apply |
| First Name | No | Used if creating new subscriber |
| Last Name | No | Used if creating new subscriber |
**Example use cases:**
* Tag subscribers as "webinar-attendee" when they register on Eventbrite
* Tag customers as "high-value" when they make a large purchase
* Tag users as "churned" when they cancel in your billing system
### Remove Tag from Subscriber
Remove a tag from a subscriber. If the supplied email does not match an existing subscriber, Sequenzy creates that subscriber without the requested tag. An External ID must identify an existing subscriber.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Subscriber's email address. A missing subscriber is created without the requested tag. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Must identify an existing subscriber. |
| Tag | Yes | Tag name to remove |
**Example use cases:**
* Remove "trial" tag when user becomes a customer
* Remove "active" tag when subscription ends
* Clean up temporary campaign tags
### Trigger Event for Subscriber
Trigger a custom event that can start automations in Sequenzy. Creates the subscriber when an email address is provided.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Subscriber's email address. Required when creating a new subscriber. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Can identify an existing subscriber. |
| Event Name | Yes | Name of the event (e.g., `purchase_completed`) |
| Event Properties | No | Additional data about the event (JSON) |
| First Name | No | Used if creating new subscriber |
| Last Name | No | Used if creating new subscriber |
**Example use cases:**
* Trigger `onboarding_started` when a user signs up in your app
* Trigger `trial_ending` 3 days before trial expiration
* Trigger `feature_used` when users engage with specific features
### Delete Subscriber
Remove a subscriber from your list.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Subscriber's email address. Required unless External ID is provided. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Required unless Email is provided. |
**Example use cases:**
* Remove unsubscribes synced from other systems
* Clean up test accounts automatically
* Handle GDPR deletion requests from other platforms
## Available Searches
### Find Subscriber
Look up a subscriber by email or external ID. Useful for checking if someone exists before taking action.
| Field | Required | Description |
| - | - | - |
| Email | Conditional | Email address to search for. Required unless External ID is provided. |
| External ID | Conditional | Your app/customer/user ID for this subscriber. Required unless Email is provided. |
**Returns:** Subscriber data including name, tags, custom attributes, and status.
## Auto-Creation Feature
The **Create or Update Subscriber**, **Add Tag**, **Remove Tag**, and **Trigger Event** actions can create subscribers if the action includes an email address. **Remove Tag** creates a missing subscriber without adding the requested tag. This means you can:
* Tag users without first creating them
* Trigger events for users who haven't been added yet
* Build simpler Zaps without checking if subscriber exists first
If you use only External ID, the subscriber must already exist. **Find Subscriber** and **Delete Subscriber** do not create subscribers.
## Recommended Zap Templates
These are the recommended templates to publish or promote first:
* **Typeform → Create or Update Subscriber**: Add form respondents to Sequenzy and map answers into custom attributes.
* **Tally → Create or Update Subscriber**: Capture Tally form leads with source and form metadata.
* **Facebook Lead Ads → Create or Update Subscriber**: Send new ad leads to Sequenzy with campaign/ad set fields as attributes.
* **Webflow → Create or Update Subscriber**: Add website form submissions with a `webflow-lead` tag.
* **Stripe → Add Tag / Trigger Event**: Add lifecycle tags such as `customer` or trigger purchase/subscription events for automations.
* **Google Sheets → Create or Update Subscriber**: Add or update subscribers from new or updated spreadsheet rows.
## Multi-Step Zaps
You can combine multiple Sequenzy actions in a single Zap:
* Create or update the subscriber
* Add relevant tags
* Remove old tags
* Trigger a custom event
## Troubleshooting
### Subscriber Not Created
1. Verify the email address is valid
2. Check that your API key has the correct permissions
3. Ensure the Zap is turned on
### Tags Not Appearing
1. Tags are case-sensitive - "Customer" and "customer" are different
2. Check the tag field isn't empty
3. Verify the subscriber exists (or use an action that auto-creates)
### Event Not Triggering Automation
1. Verify you have an automation set up for that event in Sequenzy
2. Check the event name matches exactly (case-sensitive)
3. Ensure the subscriber meets any conditions on the automation
### Authentication Failed
1. Verify your API key is correct (current keys start with `seq_live_` or `seq_user_`; legacy `ek_` keys are also supported)
2. Try regenerating a new API key in **Settings → API Keys & SMTP**
3. Reconnect your Sequenzy account in Zapier
## FAQ
Yes. New Subscriber and Campaign Sent are instant triggers. New Subscriber
covers every creation source and waits for confirmation when double opt-in
is enabled. Campaign Sent fires once per completed email campaign.
The two triggers use instant webhooks rather than polling. Actions are also
sent immediately; normal network and Zap processing time still applies.
Yes, the Zapier integration works with all Sequenzy plans.
Zapier handles action-side rate limits. For trigger delivery, Sequenzy makes
up to 16 attempts with exponential backoff spanning 7 days. For high-volume
imports, use the bulk API rather than one Zap task per contact.
Yes, you can connect multiple Sequenzy accounts to Zapier by using different
API keys.
Yes. Sequenzy has a Make custom app for subscriber, tag, and event actions.
Public Make Marketplace availability depends on Make's review; until then,
install it as a private custom app from the connector source. For a
self-hostable workflow builder, use the [n8n
integration](/integrations/n8n).
Yes. Start with New Subscriber or Campaign Sent, then add actions for the
destination app. Use Sequenzy outbound webhooks directly when you need an
event that the Zapier app does not yet expose.
# Introduction
Source: https://docs.sequenzy.com/introduction
Sequenzy is email marketing for developers: sequences, campaigns, and transactional email from one dashboard, API, CLI, or AI agent
Sequenzy sends lifecycle email for your product. Automate onboarding and
billing journeys, send campaigns to targeted segments, and deliver
transactional email such as receipts and password resets. Most of what you do
in the dashboard is also available through the API, the CLI, and AI agents
over MCP.
## What you can build
Automated journeys triggered by signups, events, tags, or segments
One-time email and SMS broadcasts to lists and segments
Receipts, password resets, and notifications sent from your app
Contacts with tags, custom attributes, lists, and activity history
Track what people do in your product and react to it
Deliverability, opens, clicks, and conversions for every email
## How it fits together
```mermaid theme={null}
flowchart LR
App["Your app, billing, and auth providers"] -->|subscribers and events| API["Sequenzy API"]
Agent["AI agents (MCP) and CLI"] --> API
API --> Seq["Sequences"]
API --> Camp["Campaigns"]
API --> Tx["Transactional email"]
Seq --> Inbox["Your subscribers' inboxes"]
Camp --> Inbox
Tx --> Inbox
```
Your app sends subscribers and events to Sequenzy, directly or through
[integrations](/integrations/stripe) such as Stripe, Clerk, and Supabase.
Sequences react to those events, campaigns reach the segments you choose, and
transactional email goes out the moment your app asks for it.
## Choose your path
Create an API key, add a subscriber, and send a transactional email in about
five minutes
Connect ChatGPT, Claude, Cursor, Codex, or another MCP client
Connect your auth and billing providers so email reacts to signups, trials,
and payments
Move transactional email, sequences, contacts, and campaigns in phases
## API basics
All API requests go to `https://api.sequenzy.com/api/v1` and authenticate with
an API key. See [Authentication](/authentication) for key types and scopes, or
browse the [API reference](/api-reference/introduction).
## Need help?
* **Email**: [nic@sequenzy.com](mailto:nic@sequenzy.com)
* **X**: [@nikpolale](https://x.com/nikpolale)
* **Changelog**: [What's new](/changelog)
# Quick Start
Source: https://docs.sequenzy.com/quickstart
Create an API key, add a subscriber, track an event, and send your first transactional email in about five minutes
This guide takes you from a new account to your first API-sent email. You need a
Sequenzy account ([sign up here](https://www.sequenzy.com)).
You can send right away from the built-in `sequenzymail.com` domain. Its
reputation is shared, so [verify your own domain](/guides/domain-verification)
in **Settings → Domain** before you send to real customers.
## Step 1: Get Your API Key
1. Log in to your [Sequenzy dashboard](https://www.sequenzy.com/dashboard)
2. Open **Settings → API Keys & SMTP**
3. Click **Create API Key**, name the key, and keep the **Full access** preset
for this guide (a **Transactional only** key cannot run Steps 3-5)
4. Copy and securely store your key. It won't be shown again
Keep your API key secret. Never expose it in client-side code or public
repositories.
Managing multiple workspaces? Create an account key in **Account Settings →
API Keys** and pass `x-company-id: company_abc123` on each request. One
`seq_user_` key can target every workspace your account can access, including
workspaces you create later.
## Step 2: Install the SDK
Install the official Sequenzy SDK for Node.js/TypeScript:
```bash npm theme={null}
npm install sequenzy
```
```bash pnpm theme={null}
pnpm add sequenzy
```
```bash bun theme={null}
bun add sequenzy
```
Then initialize the client:
```typescript theme={null}
import Sequenzy from "sequenzy";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY,
});
```
## Step 3: Add Your First Subscriber
```typescript Node SDK theme={null}
await client.subscribers.create({
email: "john@example.com",
firstName: "John",
lastName: "Doe",
tags: ["new-signup"],
customAttributes: {
plan: "free",
source: "website",
},
enrollInSequences: true,
});
```
```bash cURL theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe",
"tags": ["new-signup"],
"customAttributes": {
"plan": "free",
"source": "website"
},
"enrollInSequences": true
}'
```
## Step 4: Tag Subscribers Based on Actions
When users take actions in your app, tag them to trigger automations:
```typescript Node SDK theme={null}
await client.subscribers.tags.add({
email: "john@example.com",
tag: "customer",
});
```
```bash cURL theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/tags" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"tag": "customer"
}'
```
## Step 5: Track Events
Send events when important things happen. Send money amounts in cents, so revenue
goals and reports read them correctly:
```typescript Node SDK theme={null}
await client.subscribers.events.trigger({
email: "john@example.com",
event: "purchase_completed",
properties: {
orderId: "order_12345",
amount: 9999, // in cents
product: "Pro Plan",
},
});
```
```bash cURL theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/subscribers/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"event": "purchase_completed",
"properties": {
"orderId": "order_12345",
"amount": 9999,
"product": "Pro Plan"
}
}'
```
## Step 6: Send a Transactional Email
Send an email right away with inline content, or with a template you design in
the dashboard. The response includes an `emailSendId` you can look up in
**Sent Emails** or with [Get Email Send](/api-reference/email-sends/get).
### Direct Content
```typescript Node SDK theme={null}
await client.transactional.send({
to: "john@example.com",
subject: "Welcome to Our Platform!",
body: "
",
"variables": {
"firstName": "John"
}
}'
```
### Using a Template
Create a template in **Transactional** in the dashboard and copy its slug, then
send it by slug:
```typescript Node SDK theme={null}
await client.transactional.send({
to: "john@example.com",
slug: "welcome-email",
variables: {
firstName: "John",
dashboardUrl: "https://app.example.com/dashboard",
},
});
```
```bash cURL theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "john@example.com",
"slug": "welcome-email",
"variables": {
"firstName": "John",
"dashboardUrl": "https://app.example.com/dashboard"
}
}'
```
## What's Next?
Build automated email workflows
Broadcast to your subscribers
Automate tagging based on events
Full API documentation
# Send Email with Bun
Source: https://docs.sequenzy.com/send-email/bun
Send transactional emails using Sequenzy and Bun
This guide shows you how to send transactional emails using Sequenzy in a Bun application.
## Prerequisites
* A Sequenzy account with an [API key](/authentication)
* A [verified sending domain](/guides/domain-verification)
* [Bun](https://bun.sh) installed
The SDK returns the API response directly and throws on failed requests. Use
`try/catch` in your server handler. A successful send response means queued,
not delivered; use its `emailSendId` to check [delivery status](/api-reference/email-sends/get).
## 1. Install
Install the Sequenzy SDK:
```bash theme={null}
bun add sequenzy react @react-email/render
```
## 2. Create an Email Template
Create a React Email component for your email template:
```tsx email-template.tsx theme={null}
import * as React from "react";
interface WelcomeEmailProps {
firstName: string;
}
export function WelcomeEmail({ firstName }: WelcomeEmailProps) {
return (
Welcome, {firstName}!
Thanks for signing up. We're excited to have you on board.
);
}
```
You can use [React Email](https://react.email) components to build beautiful,
responsive email templates. See our [React Email guide](#using-react-email)
below for more details.
## 3. Send the Email
Create your Bun server and send an email:
```tsx index.tsx theme={null}
import Sequenzy from "sequenzy";
import { render } from "@react-email/render";
import { WelcomeEmail } from "./email-template";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
});
const server = Bun.serve({
port: 3000,
async fetch(request) {
// Only send on an explicit POST, never on every page load.
const { pathname } = new URL(request.url);
if (request.method !== "POST" || pathname !== "/send") {
return new Response("Not found", { status: 404 });
}
try {
const data = await client.transactional.send({
to: "user@example.com",
subject: "Welcome to Our Platform!",
body: await render(WelcomeEmail({ firstName: "John" })),
});
return new Response(JSON.stringify({ data }));
} catch (error) {
console.error("Sequenzy request failed:", error);
return Response.json(
{ error: "Email service request failed" },
{ status: 502 }
);
}
},
});
console.log(`Server running at http://localhost:${server.port}`);
```
## 4. Run
Start your server:
```bash theme={null}
bun run index.tsx
```
Then send the email:
```bash theme={null}
curl -X POST http://localhost:3000/send
```
## Using Templates
Instead of rendering HTML directly, you can use pre-built templates from the Sequenzy dashboard:
```tsx theme={null}
const data = await client.transactional.send({
to: "user@example.com",
slug: "welcome-email",
variables: {
firstName: "John",
dashboardUrl: "https://app.example.com/dashboard",
},
});
```
## Using React Email
[React Email](https://react.email) provides a set of components for building responsive emails. Install it alongside Sequenzy:
```bash theme={null}
bun add @react-email/components @react-email/render
```
Create a more sophisticated template:
```tsx email-template.tsx theme={null}
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";
import * as React from "react";
interface WelcomeEmailProps {
firstName: string;
dashboardUrl: string;
}
export function WelcomeEmail({ firstName, dashboardUrl }: WelcomeEmailProps) {
return (
Welcome to our platformWelcome, {firstName}!
Thanks for signing up. Get started by visiting your dashboard:
);
}
```
Then render and send:
```tsx theme={null}
import { render } from "@react-email/render";
import { WelcomeEmail } from "./email-template";
const html = await render(
WelcomeEmail({
firstName: "John",
dashboardUrl: "https://app.example.com/dashboard",
})
);
await client.transactional.send({
to: "user@example.com",
subject: "Welcome to Our Platform!",
body: html,
});
```
## Next Steps
Explore all available React Email components
Learn more about transactional emails
# Send Email with Express
Source: https://docs.sequenzy.com/send-email/express
Send transactional emails using Sequenzy and Express
This guide shows you how to send transactional emails using Sequenzy in an Express application.
## Prerequisites
* A Sequenzy account with an [API key](/authentication)
* A [verified sending domain](/guides/domain-verification)
* Node.js 18+ installed
The SDK returns the API response directly and throws on failed requests. Use
`try/catch` in your server handler. A successful send response means queued,
not delivered; use its `emailSendId` to check [delivery status](/api-reference/email-sends/get).
## 1. Install
Install the Sequenzy SDK and Express:
```bash npm theme={null}
npm install sequenzy express
```
```bash pnpm theme={null}
pnpm add sequenzy express
```
```bash bun theme={null}
bun add sequenzy express
```
For TypeScript support:
```bash theme={null}
npm install -D @types/express typescript tsx
```
## 2. Create Your Server
Create a basic Express server that sends emails:
```ts index.ts theme={null}
import express, { Request, Response } from "express";
import Sequenzy from "sequenzy";
const app = express();
app.use(express.json());
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
});
app.post("/send", async (req: Request, res: Response) => {
const { email, firstName } = req.body;
try {
const data = await client.transactional.send({
to: email,
subject: "Welcome to Our Platform!",
body: "
Welcome, {{firstName}}!
Thanks for signing up.
",
variables: { firstName },
});
return res.status(200).json({ data });
} catch (error) {
console.error("Sequenzy request failed:", error);
return res.status(502).json({ error: "Email service request failed" });
}
});
app.listen(3000, () => {
console.log("Server running at http://localhost:3000");
});
```
## 3. Run
Start your server:
```bash theme={null}
npx tsx index.ts
```
Send a test request:
```bash theme={null}
curl -X POST http://localhost:3000/send \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "firstName": "John"}'
```
## Using Templates
Instead of inline HTML, use pre-built templates from the Sequenzy dashboard:
```ts theme={null}
app.post("/send-welcome", async (req: Request, res: Response) => {
const { email, firstName } = req.body;
try {
const data = await client.transactional.send({
to: email,
slug: "welcome-email",
variables: {
firstName,
dashboardUrl: "https://app.example.com/dashboard",
},
});
return res.status(200).json({ data });
} catch (error) {
console.error("Sequenzy request failed:", error);
return res.status(502).json({ error: "Email service request failed" });
}
});
```
## Using React Email
For complex email templates, use [React Email](https://react.email) with Express:
```bash theme={null}
npm install @react-email/components @react-email/render
```
Create a template:
```tsx emails/welcome-email.tsx theme={null}
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";
import * as React from "react";
interface WelcomeEmailProps {
firstName: string;
dashboardUrl: string;
}
export function WelcomeEmail({ firstName, dashboardUrl }: WelcomeEmailProps) {
return (
Welcome to our platformWelcome, {firstName}!
Thanks for signing up. Get started by visiting your dashboard:
);
}
```
Use it in your route:
```ts theme={null}
import { render } from "@react-email/render";
import { WelcomeEmail } from "./emails/welcome-email";
app.post("/send-welcome", async (req: Request, res: Response) => {
const { email, firstName } = req.body;
const html = await render(
WelcomeEmail({
firstName,
dashboardUrl: "https://app.example.com/dashboard",
})
);
try {
const data = await client.transactional.send({
to: email,
subject: "Welcome to Our Platform!",
body: html,
});
return res.status(200).json({ data });
} catch (error) {
console.error("Sequenzy request failed:", error);
return res.status(502).json({ error: "Email service request failed" });
}
});
```
## Error Handling
Add proper error handling for production:
```ts theme={null}
app.post("/send", async (req: Request, res: Response) => {
try {
const { email, firstName } = req.body;
if (!email || !firstName) {
return res
.status(400)
.json({ error: "Email and firstName are required" });
}
const data = await client.transactional.send({
to: email,
slug: "welcome-email",
variables: { firstName },
});
return res.status(200).json({ data });
} catch (err) {
console.error("Unexpected error:", err);
return res.status(500).json({ error: "Internal server error" });
}
});
```
## Next Steps
Explore all available React Email components
Learn more about transactional emails
# Send Email with Next.js
Source: https://docs.sequenzy.com/send-email/nextjs
Send transactional emails using Sequenzy and Next.js
This guide shows you how to send transactional emails using Sequenzy in a Next.js application.
## Prerequisites
* A Sequenzy account with an [API key](/authentication)
* A [verified sending domain](/guides/domain-verification)
* A Next.js application
The SDK returns the API response directly and throws on failed requests. Use
`try/catch` in your server handler. A successful send response means queued,
not delivered; use its `emailSendId` to check [delivery status](/api-reference/email-sends/get).
## 1. Install
Install the Sequenzy SDK and React Email:
```bash npm theme={null}
npm install sequenzy @react-email/components @react-email/render
```
```bash pnpm theme={null}
pnpm add sequenzy @react-email/components @react-email/render
```
```bash bun theme={null}
bun add sequenzy @react-email/components @react-email/render
```
## 2. Configure Environment
Add your API key to `.env.local`:
```bash .env.local theme={null}
SEQUENZY_API_KEY=your_api_key_here
```
## 3. Create an Email Template
Create a React Email component:
```tsx components/emails/welcome-email.tsx theme={null}
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";
import * as React from "react";
interface WelcomeEmailProps {
firstName: string;
dashboardUrl: string;
}
export function WelcomeEmail({ firstName, dashboardUrl }: WelcomeEmailProps) {
return (
Welcome to our platformWelcome, {firstName}!
Thanks for signing up. Get started by visiting your dashboard:
);
}
```
## 4. Create an API Route
As written, these routes send to any address a caller provides. Before you
deploy, authenticate the request, send only to the signed-in user (or another
address you trust), and rate limit the route. Otherwise anyone can use it to
send email from your domain.
### App Router
Create an API route at `app/api/send/route.ts`:
```ts app/api/send/route.ts theme={null}
import { NextResponse } from "next/server";
import Sequenzy from "sequenzy";
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/components/emails/welcome-email";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
});
export async function POST(request: Request) {
const { email, firstName } = await request.json();
const html = await render(
WelcomeEmail({
firstName,
dashboardUrl: "https://app.example.com/dashboard",
})
);
try {
const data = await client.transactional.send({
to: email,
subject: "Welcome to Our Platform!",
body: html,
});
return NextResponse.json({ data });
} catch (error) {
console.error("Sequenzy request failed:", error);
return NextResponse.json(
{ error: "Email service request failed" },
{ status: 502 }
);
}
}
```
### Pages Router
Create an API route at `pages/api/send.ts`:
```ts pages/api/send.ts theme={null}
import type { NextApiRequest, NextApiResponse } from "next";
import Sequenzy from "sequenzy";
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/components/emails/welcome-email";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const { email, firstName } = req.body;
const html = await render(
WelcomeEmail({
firstName,
dashboardUrl: "https://app.example.com/dashboard",
})
);
try {
const data = await client.transactional.send({
to: email,
subject: "Welcome to Our Platform!",
body: html,
});
return res.status(200).json({ data });
} catch (error) {
console.error("Sequenzy request failed:", error);
return res.status(502).json({ error: "Email service request failed" });
}
}
```
## 5. Send the Email
Call your API route from anywhere in your application:
```ts theme={null}
const response = await fetch("/api/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "user@example.com",
firstName: "John",
}),
});
const result = await response.json();
```
## Using Templates
Instead of rendering HTML with React Email, you can use pre-built templates from the Sequenzy dashboard:
```ts theme={null}
const data = await client.transactional.send({
to: email,
slug: "welcome-email",
variables: {
firstName: "John",
dashboardUrl: "https://app.example.com/dashboard",
},
});
```
This approach lets non-developers update email templates without code changes.
## Server Actions (App Router)
You can also send emails using Server Actions:
```ts app/actions/send-email.ts theme={null}
"use server";
import Sequenzy from "sequenzy";
import { render } from "@react-email/render";
import { WelcomeEmail } from "@/components/emails/welcome-email";
const client = new Sequenzy({
apiKey: process.env.SEQUENZY_API_KEY!,
});
export async function sendWelcomeEmail(email: string, firstName: string) {
const html = await render(
WelcomeEmail({
firstName,
dashboardUrl: "https://app.example.com/dashboard",
})
);
const data = await client.transactional.send({
to: email,
subject: "Welcome to Our Platform!",
body: html,
});
return data;
}
```
Then use it in your component:
```tsx theme={null}
import { sendWelcomeEmail } from "@/app/actions/send-email";
export function SignupForm() {
async function onSubmit(formData: FormData) {
"use server";
const email = formData.get("email") as string;
const firstName = formData.get("firstName") as string;
await sendWelcomeEmail(email, firstName);
}
return (
);
}
```
## Next Steps
Explore all available React Email components
Learn more about transactional emails
# Send with SMTP
Source: https://docs.sequenzy.com/send-email/smtp
Submit transactional emails to Sequenzy using SMTP
Send transactional emails into Sequenzy via SMTP instead of the REST API. This
is useful for applications that already have SMTP integration (Laravel,
WordPress, legacy systems) or prefer the standard email protocol.
This page describes **SMTP submission to Sequenzy**: your application connects
to `smtp.sequenzy.com`, then Sequenzy handles outbound delivery. Sequenzy does
not support customer-managed outbound SMTP relays: outbound delivery remains
Sequenzy-managed through SES or Sequenzy's MTA. You cannot configure Hostinger
or another mailbox provider as the delivery transport for campaigns,
sequences, or transactional sends.
You can still use a mailbox or alias hosted by Hostinger or another provider as
the visible From address after verifying its domain in Sequenzy, and as the
Reply-To address for manual replies. Your existing mailbox hosting can remain
unchanged.
## SMTP Credentials
Use these credentials to configure your application:
| Setting | Value |
| - | - |
| **Host** | `smtp.sequenzy.com` |
| **Username** | `api` |
| **Password** | Your API key |
The username can be anything: only the password (your API key) is used for
authentication. We recommend `api` for clarity.
SMTP requires a company API key with the `transactional:send` permission.
Full-access keys and keys created with the `transactional_sender` preset both
work. Personal keys and keys without that permission are rejected at login.
## Port Options
Sequenzy supports multiple ports for different network environments:
| Port | Security | Description |
| - | - | - |
| **587** | STARTTLS | **Recommended.** Starts unencrypted, upgrades to TLS. |
| **465** | Implicit TLS | Encrypted from connection start (legacy SMTPS). |
| **2525** | STARTTLS | Fallback port for firewalled networks (ISPs blocking 587). |
### Which port should I use?
* **Port 587** is the standard for email submission and works with most applications
* **Port 465** is for legacy applications that require implicit TLS (connect with SSL from the start)
* **Port 2525** is useful if your ISP or firewall blocks standard SMTP ports. It uses STARTTLS just like port 587.
## Prerequisites
Before sending via SMTP:
1. **Create an API key** in [the Sequenzy dashboard](https://www.sequenzy.com/dashboard). Go to **Settings → API Keys & SMTP** to view your connection details and generate the key (it doubles as your SMTP password) in one click, or manage your keys on the same page
2. **Verify your domain** in **Settings → Domain**
3. **Configure a sender profile** with the email address you'll send from
## Quick Start
```javascript Node.js (Nodemailer) theme={null}
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "smtp.sequenzy.com",
port: 587,
secure: false, // true for port 465, false for 587/2525
auth: {
user: "api",
pass: process.env.SEQUENZY_API_KEY,
},
});
await transporter.sendMail({
from: "you@your-verified-domain.com",
to: "recipient@example.com",
subject: "Hello from Sequenzy",
html: "
Hello!
This email was sent via SMTP.
",
});
```
```python Python (smtplib) theme={null}
import smtplib
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
msg = MIMEMultipart("alternative")
msg["Subject"] = "Hello from Sequenzy"
msg["From"] = "you@your-verified-domain.com"
msg["To"] = "recipient@example.com"
html = "
\r\n")
smtp.SendMail("smtp.sequenzy.com:587", auth, "you@your-verified-domain.com", to, msg)
}
```
```ruby Ruby (mail gem) theme={null}
require 'mail'
Mail.defaults do
delivery_method :smtp, {
address: 'smtp.sequenzy.com',
port: 587,
user_name: 'api',
password: ENV['SEQUENZY_API_KEY'],
authentication: :plain,
enable_starttls_auto: true
}
end
Mail.deliver do
from 'you@your-verified-domain.com'
to 'recipient@example.com'
subject 'Hello from Sequenzy'
html_part do
content_type 'text/html; charset=UTF-8'
body '
Hello!
This email was sent via SMTP.
'
end
end
```
```bash cURL (swaks) theme={null}
# Install: brew install swaks (macOS) or apt install swaks (Ubuntu)
swaks --to recipient@example.com \
--from you@your-verified-domain.com \
--server smtp.sequenzy.com \
--port 587 --tls \
--auth LOGIN \
--auth-user "api" \
--auth-password "$SEQUENZY_API_KEY" \
--header "Subject: Hello from Sequenzy" \
--body "This is a test email sent via SMTP"
```
## Framework Integrations
### Laravel
Update your `.env` file:
```bash theme={null}
MAIL_MAILER=smtp
MAIL_HOST=smtp.sequenzy.com
MAIL_PORT=587
MAIL_USERNAME=api
MAIL_PASSWORD=seq_live_your_api_key
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=you@your-verified-domain.com
MAIL_FROM_NAME="Your App"
```
Then send emails using Laravel's Mail facade:
```php theme={null}
Mail::to('recipient@example.com')->send(new WelcomeEmail());
```
### Django
Update your `settings.py`:
```python theme={null}
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sequenzy.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'api'
EMAIL_HOST_PASSWORD = os.environ.get('SEQUENZY_API_KEY')
DEFAULT_FROM_EMAIL = 'you@your-verified-domain.com'
```
Then send emails:
```python theme={null}
from django.core.mail import send_mail
send_mail(
'Hello from Sequenzy',
'Plain text body',
'you@your-verified-domain.com',
['recipient@example.com'],
html_message='
Hello!
This email was sent via SMTP.
'
)
```
### Ruby on Rails
Update `config/environments/production.rb`:
```ruby theme={null}
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.sequenzy.com',
port: 587,
user_name: 'api',
password: ENV['SEQUENZY_API_KEY'],
authentication: :plain,
enable_starttls_auto: true
}
```
### WordPress
Install a plugin like [WP Mail SMTP](https://wordpress.org/plugins/wp-mail-smtp/) and configure:
* **SMTP Host**: `smtp.sequenzy.com`
* **Encryption**: TLS
* **SMTP Port**: 587
* **SMTP Username**: `api`
* **SMTP Password**: Your API key
## Features
### Same Pipeline as REST API
SMTP emails go through the same processing pipeline:
* **Analytics**: All emails appear in your Sent Emails dashboard
* **Tracking**: Opens and clicks are tracked (if enabled)
* **Branding**: Footer added for free tier accounts
* **Rate limits**: Same limits apply as REST API
### Security Enforcement
The SMTP server applies the same security checks:
| Check | Description |
| - | - |
| API key validation | Password must be a valid API key |
| Domain verification | Sender domain must be verified |
| Bounce protection | Emails to known bounced addresses are rejected |
| Rate limiting | 100 emails/minute per company |
| Account status | Paused/suspended accounts cannot send |
### Message Limits
| Limit | Value |
| - | - |
| Max message size | 25 MB |
| Max To recipients | 50 |
## Rate Limits
| Limit | Value |
| - | - |
| Emails per minute | 100 (per company) |
Repeated failed logins are slowed down progressively. A valid key is always
accepted.
## Troubleshooting
Your ISP or firewall may be blocking SMTP ports. Try port 2525 instead of 587.
* Use a workspace API key as the password. It starts with `seq_live_` (older
keys start with `ek_`).
* Account keys (`seq_user_`) are not accepted for SMTP.
* Check that the key hasn't been revoked in your dashboard.
* Connect with STARTTLS (port 587 or 2525) or TLS (port 465). Plain connections
cannot authenticate.
The sender domain (the domain in your From address) must be verified in
**Settings → Domain** before you can send.
The recipient email may be on the bounce list. Check your Sent Emails
dashboard for previous bounces to this address.
You've exceeded 100 emails/minute. Wait a minute and try again, or contact support to increase your limit.
## FAQ
Yes, the username is ignored: only the password (your API key) is used for authentication. We recommend using `api` for clarity.
Yes, all emails sent via SMTP appear in your Sent Emails dashboard with full
analytics, same as REST API emails.
Yes, if tracking is enabled for your account. SMTP emails use the same
tracking pixel and link rewriting as REST API emails.
Yes, standard MIME attachments are forwarded with the message. You can attach
up to 10 files, 15MB total across all of them. A message over those limits is
rejected at the SMTP transaction so you find out immediately, rather than
being delivered without the files.
Yes. Send the message as `multipart/related` with a `Content-ID` on each embedded part and reference it from your HTML as ``. Sequenzy keeps the `cid:` reference and re-sends the part as a related attachment with the same Content-ID, so it renders in Gmail, Outlook, and Apple Mail.
No setting or extra header is needed - this is the default. Embedded images count toward the same 10 file / 15MB attachment limits.
* **Port 587 (STARTTLS)**: Starts unencrypted, then upgrades to TLS. This is the modern standard.
* **Port 465 (Implicit TLS)**: Encrypted from the start. This is a legacy method but still supported.
Use port 587 unless your application specifically requires implicit TLS.
Some ISPs and corporate firewalls block standard SMTP ports (25, 587, 465).
Port 2525 is an alternative that's less likely to be blocked and still uses
STARTTLS like port 587.
SMTP is designed for transactional emails. For bulk marketing campaigns, use the Campaign feature in the dashboard or REST API.
# Popup Widget
Source: https://docs.sequenzy.com/widgets/popup-widget
Create a hosted popup signup form and install it with a script tag.
The Popup Widget lets you create a signup popup inside Sequenzy and
install it on any website with a hosted script.
## Quick Start
1. Open **Pages & Forms → Popups** in your Sequenzy dashboard.
2. Create a popup or open an existing one.
3. Pick a template, trigger, fields, lists, tags, and success message.
4. Copy the generated HTML snippet.
5. Paste it into a site template that permits third-party hosted scripts.
Sequenzy popup code is not part of the Webflow Marketplace integration. Do not
paste it into Webflow. Use [native Webflow form
capture](/integrations/webflow) instead. While Webflow is connected, custom
popup embed code and its submission endpoint are disabled for that workspace,
including previously deployed snippets.
## Install Code
The generated snippet loads the popup's saved configuration and runtime from
the Sequenzy application domain:
```html theme={null}
```
Replace `popup_123` with the saved popup ID. The code panel in the popup
builder supplies the complete versioned snippet automatically.
## Triggers
You can open the popup after a delay, after the visitor scrolls, on exit intent,
or manually from your own button:
```html theme={null}
```
## Submissions
Saved popup submissions post to the saved popup endpoint:
```text theme={null}
POST https://api.sequenzy.com/api/v1/forms/popups/{popupId}
```
This means list targeting, tag assignment, custom fields, duplicate handling,
spam honeypot protection, and existing subscriber updates use the popup settings
stored in Sequenzy.
A contact who unsubscribed from all email and submits the popup again is
resubscribed and rejoins the popup's lists. Turn on **Require email confirmation
to resubscribe** under **Data > Advanced** in the popup builder, or set `resubscribeBehavior`
to `double_opt_in`, to send them your double opt-in confirmation email first.
See [Returning unsubscribed contacts](./signup-form#returning-unsubscribed-contacts).
## Branding
Saved popups show a small **Powered by Sequenzy** link for free workspaces.
Paid workspaces are white-label. The hosted script checks the current workspace
plan when it loads, so upgrading removes the branding without requiring you to
replace the embed snippet.
# Signup Form Widget
Source: https://docs.sequenzy.com/widgets/signup-form
Embed a customizable signup form with JavaScript, HTML, or a native static-site form action
The Signup Form Widget is a customizable form that you can embed on any website to collect email subscribers. Use a one-line JavaScript embed, copy the full HTML, or post a native form directly from a static site.
Forms are saved in your dashboard. Each form has its own submission endpoint, and the audience (lists, tags) and success behavior (message or redirect) are stored server-side. When you edit those settings later, deployed embeds pick up the changes automatically - no re-embedding needed. Only visual changes (colors, placeholders, layout) require copying the code again.
For popup capture, use the [Popup Widget](/widgets/popup-widget). It uses the
same Forms API for submissions but loads a hosted JavaScript runtime.
## Template Gallery
Start from a saved form template, then customize the fields, layout, and styling
in the form editor:
## Features
JavaScript, HTML, native form action, and fetch options
Configure colors, button text, and layout
Built-in honeypot field to prevent bot submissions
Add subscribers to specific lists or all lists
Apply existing tags when someone subscribes
Require email confirmation before subscribers become active
## Quick Start
### Step 1: Open Forms
Navigate to **Pages & Forms → Forms** in your [Sequenzy dashboard](https://www.sequenzy.com/dashboard) and click **New form**.
### Step 2: Customize Your Form
Configure your form using the visual builder:
* **Form name**: Name the form so you can tell your signup forms apart
* **Form Style**: Choose between "Button below" (stacked) or "Button inline" (side-by-side)
* **Placeholder Text**: Customize the email input placeholder
* **Custom Fields**: Add text fields such as nickname, zipcode, shipping address, or favorite fruit
* **Font Color**: Set the input text color
* **Button Text**: Customize the submit button label
* **Button Color**: Set the button background color
* **After submission**: Show a success message or redirect visitors to a URL
* **Success Message**: Customize the confirmation message shown after submission
* **Subscribe to**: Choose which list(s) new subscribers should be added to
* **Apply Tags**: Choose which existing tags new subscribers should receive
* **Double Opt-In**: Forms follow the workspace setting in **Settings → Tracking**
### Step 3: Save and Embed
Click **Save & Embed** to choose where the form should be installed.
#### Choose A Destination
Add the form directly to a Sequenzy landing page, or select **Website and
platform snippets** for an external site.
#### Copy The Installation Snippet
Choose the embed option for your site, then copy the generated code:
* **JavaScript**: one script tag that renders the latest saved form settings
* **HTML**: the complete copy-paste form markup
* **Static site / Form action**: a public action URL, minimal native form, and
optional fetch enhancement for Astro, Hugo, Jekyll, Cloudflare Pages,
Netlify, and GitHub Pages
## Static Sites and Form Actions
Saved forms are client-safe by design. The opaque `formId` is the public
capability, while lists, tags, duplicate handling, and success behavior remain
stored in Sequenzy. Your Astro, Hugo, Jekyll, or plain HTML page never needs a
Bearer token, server function, or secret environment variable.
Open a saved form, choose **Embed → Static site / Form action**, and copy the
native form:
```html theme={null}
```
This works with JavaScript disabled. When you want an inline success state,
copy the Fetch example from the same tab; it submits `FormData` directly to the
public action and still contains no API key.
### Managing Saved Forms
Saved forms appear on **Pages & Forms → Forms**, with their submission counts. From there you can:
* **Edit** a form's design, audience, and success behavior
* **Embed** an existing form without changing it
* **Delete** a form. Deployed embeds of a deleted form stop accepting submissions.
### Webflow
Do not paste a Sequenzy HTML, React, JavaScript, or form-action snippet into
Webflow. The Marketplace integration supports native Webflow forms only. Use
the [Webflow integration](/integrations/webflow) to select forms, fields,
consent, lists, and sequence behavior through official Webflow APIs. While a
workspace is connected to Webflow, Sequenzy does not return custom form embed
code and rejects submissions to that workspace's existing Sequenzy form embed
endpoints.
## Generated HTML
The form builder generates clean, self-contained HTML like this:
```html theme={null}
```
## Form Layouts
### Button Below (Default)
The input and button are stacked vertically:
```
┌─────────────────────────┐
│ you@example.com │
└─────────────────────────┘
┌─────────────────────────┐
│ Submit form │
└─────────────────────────┘
```
### Button Inline
The input and button are side-by-side:
```
┌─────────────────────┐ ┌────────────┐
│ you@example.com │ │ Submit │
└─────────────────────┘ └────────────┘
```
## Targeting Specific Lists
When you create a form, all current lists are selected by default. You can keep
that selection, choose a smaller set of lists, or clear the selection to create
subscribers without adding them to any list.
1. In the form builder, use the **Subscribe to** picker to select or clear lists
2. Click **Save & Embed**
List targeting is stored with the form, not in the embed code. Changing the
selection later applies to forms already deployed on your website.
## Applying Tags
You can also apply existing tags when someone submits the form:
1. In the form builder, select tags from **Apply tags**
2. Click **Save & Embed**
Like list targeting, tags are stored with the form, so you can change them
without touching the embed code.
## Handling Existing Contacts
Saved forms can choose how submitted name fields affect an existing contact:
* `skip` preserves the existing contact fields
* `merge` fills only fields that are currently empty
* `overwrite` replaces submitted fields
The strategy is stored server-side with the saved form. Visitors cannot change
it by editing the public action URL or submitted fields.
The strategy only affects profile fields. It never changes whether a contact is
subscribed.
## Returning Unsubscribed Contacts
Submitting a form again counts as a fresh opt-in. When a contact who
unsubscribed from all email submits your form, Sequenzy resubscribes them and
restores the form's lists. Welcome automations for those lists start again.
To ask these contacts to confirm first, turn on **Require email confirmation to
resubscribe** under **Data > Advanced** in the form builder, or set `resubscribeBehavior` to
`double_opt_in` through the API, CLI, or MCP. They then get your double opt-in
confirmation email and are resubscribed once they click it. This option needs a
confirmation email and a sender profile. If the confirmation email does not
exist yet, create it with **Create confirmation email** next to the option.
The option applies to that form or popup only. Landing pages and older
company-level embed forms always resubscribe returning contacts; turn on
workspace double opt-in to require confirmation on every signup surface.
When workspace double opt-in is on, returning contacts always confirm first.
Contacts whose address bounced or who marked your email as spam are never
resubscribed. The form responds the same way in every case, so it does not
reveal whether an address was subscribed.
## Double Opt-In
Signup forms follow the workspace double opt-in setting. When double opt-in is enabled, a form submission creates a pending subscriber and sends your confirmation email. The subscriber becomes active only after clicking the confirmation link.
Selected lists and tags are applied after confirmation, not before. This keeps list-triggered and tag-triggered automations from starting until the subscriber has confirmed.
Learn how to enable and customize confirmation emails.
## Spam Protection
The form includes a honeypot field that's hidden from real users but visible to bots:
```html theme={null}
```
**Do not remove** the honeypot field. It helps prevent spam submissions by
catching automated bots that fill in all form fields.
## After Submission
After successful submission, users can see a success message or be redirected
to a custom page.
For success messages, you can customize:
* **Success Message**: The text shown after submission (e.g., "Thanks for subscribing!")
* **Font Color**: The color of the success message
* **Font Size**: The size of the success message text
For redirects, choose **Redirect to URL** and enter any `http` or `https` URL,
such as `https://example.com/thank-you`. You can also enter a domain like
`google.com`; Sequenzy will treat it as `https://google.com`. Redirects only
run after the submission succeeds.
The success behavior is stored with the form, so switching between message and
redirect (or changing the redirect URL) applies to deployed embeds immediately.
When a plain HTML embed is set to show a message, visitors see a hosted
confirmation page with your success message and a link back to your site.
## Free Plan Branding
On the free plan, generated embed code includes a small "Powered by Sequenzy"
pill below the form, and the hosted confirmation page shows the same pill.
Paid workspaces get unbranded forms.
The embed code is a snapshot: after upgrading, regenerate and re-paste the code
to remove the pill from forms already deployed on your site. The hosted
confirmation page updates automatically.
## Custom Styling
The generated HTML uses inline styles for maximum compatibility. You can override these styles with CSS:
```css theme={null}
/* Custom input styling */
.sequenzy-form-input {
border-color: #3b82f6 !important;
border-radius: 8px !important;
}
/* Custom button styling */
.sequenzy-form-button {
border-radius: 8px !important;
text-transform: uppercase !important;
}
/* Custom container styling */
.sequenzy-form-container {
padding: 20px;
background: #f9fafb;
border-radius: 12px;
}
```
## Integration Examples
### WordPress
Paste the HTML embed into a Custom HTML block in the WordPress block editor.
### Squarespace
Use a Code Block to add the HTML.
### Static HTML Sites
Use **Static site / Form action** and paste the native form directly into your
HTML file. The same option works in Astro, Hugo, Jekyll, Cloudflare Pages,
Netlify, and GitHub Pages without a backend proxy.
## API Endpoint
JavaScript embeds load the saved form runtime from:
```
GET https://api.sequenzy.com/api/v1/forms/{formId}/embed.js
```
Saved forms submit to a per-form endpoint:
```
POST https://api.sequenzy.com/api/v1/forms/{formId}
```
**Form Fields:**
| Field | Type | Required | Description |
| - | - | - | - |
| `email` | string | Yes | Subscriber's email address |
| `firstName` | string | No | Subscriber's first name |
| `lastName` | string | No | Subscriber's last name |
| `customAttributes[fieldKey]` | string | No | Configured custom subscriber field |
| `website` | string | No | Honeypot field (should be empty) |
Generated saved-form HTML renders built-in name fields first, then the email
field, then configured custom fields before the submit button. Custom fields
cannot currently be interleaved with built-in fields.
Lists, tags, and the redirect URL come from the form's saved settings, so they
are not part of the request. See the [Submit Saved
Form](/api-reference/widgets/submit-saved-form) and [Saved Form Embed
Script](/api-reference/widgets/saved-form-embed-script) API references for
details.
Embeds generated before saved forms existed post to `POST
/api/v1/forms/{companyId}` with lists, tags, and the redirect URL encoded in
the embed itself. Company-scoped saved form URLs also continue to work
unchanged. See [Submit Signup Form](/api-reference/widgets/submit-form).
## Best Practices
1. **Test before deploying**: Use the preview in the form builder to test your form
2. **Keep the honeypot**: Don't remove the hidden honeypot field
3. **Match your brand**: Customize colors to match your website design
4. **Clear call-to-action**: Use descriptive button text like "Subscribe" or "Get Updates"
5. **Set expectations**: Use placeholder text to indicate what you're offering
## Troubleshooting
### Form not submitting
* Ensure the form action URL is correct
* Check that required fields are filled
* Verify your domain is properly configured in Sequenzy
### Subscribers not appearing
* Check if the subscriber already exists (duplicates are ignored)
* Verify the honeypot field is empty (bots trigger this)
* Check your Sequenzy dashboard for any API errors
### Styling issues
* Inline styles have high specificity - use `!important` to override
* Some email clients strip certain CSS properties
* Test on multiple browsers and devices
## Capture submission answers
Open your form, popup or landing page and choose **Submissions** to view saved contact details and answers,
filter by an exact custom field name and answer, or export matching records.
Snapshots preserve the submitted answers even if a contact changes later.
Tracking starts with new submissions; existing profile attributes are not
backfilled as historical submissions. Deleting the contact or source removes its
records. See [List Form Submissions](../api-reference/widgets/list-capture-submissions).
Recovered historical records are marked **Reconstructed**. Their displayed date is the historical event time, and their form association is inferred. Missing answers are not filled in from current contact profiles.
# Subscription Preferences Widget
Source: https://docs.sequenzy.com/widgets/subscription-preferences
Let users manage their email subscriptions directly from your website
The Subscription Preferences Widget is an embeddable component that allows your users to manage their email subscription preferences without leaving your website. Users can subscribe/unsubscribe from email lists, cancel active sequences, and globally opt-out of all communications.
## Preview
Here's what the widget looks like when embedded in your application:
The widget displays:
* **Email preferences header** with the subscriber's masked email address
* **Email Lists** section with list-name toggles for each public preference category
* **Active Sequences** section showing any running automations with start dates
* **Save preferences** button to submit changes
* **Unsubscribe** action to stop marketing emails immediately
## Features
Users can toggle subscriptions to individual email lists
Cancel active email sequences (automations) at any time
One-click option to stop marketing emails, with a persistent confirmation
Widget automatically adjusts height based on content
## How It Works
The widget integration consists of two parts:
```
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ ┌─────────────────┐ ┌────────────────────────────┐ │
│ │ Your Backend │ │ Your Frontend │ │
│ │ │ │ │ │
│ │ 1. Get Token │─────>│ 2. Render iframe with │ │
│ │ from API │ │ token parameter │ │
│ └────────┬────────┘ └────────────────────────────┘ │
│ │ │
└───────────┼──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Sequenzy API │
│ │
│ POST /api/v1/widgets/preferences/token │
│ Returns: { success: true, token: "eyJ..." } │
│ │
└─────────────────────────────────────────────────────────────┘
```
1. **Backend**: Call the Sequenzy API to get a signed token for the logged-in user
2. **Frontend**: Embed an iframe with the token to display the preferences UI
## Dashboard Settings
Use the dashboard settings page to review the hosted preview and copy the widget
connection details before embedding it in your app.
## Quick Start
### Step 1: Get an API Key
Navigate to **Settings → API Keys & SMTP** in your [Sequenzy dashboard](https://www.sequenzy.com/dashboard) to create an API key.
### Step 2: Backend - Get Token
Create a server-side endpoint that fetches an embed token from Sequenzy:
```typescript Node.js SDK (Recommended) theme={null}
import Sequenzy from "sequenzy";
const client = new Sequenzy({ apiKey: process.env.SEQUENZY_API_KEY! });
app.post("/api/preferences-token", async (req, res) => {
const userEmail = req.user.email;
try {
const data = await client.widgets.preferences.generateToken({
email: userEmail,
});
res.json({ token: data.token });
} catch (error) {
console.error("Sequenzy request failed:", error);
return res.status(502).json({ error: "Email service request failed" });
}
});
```
```typescript Next.js (App Router) theme={null}
// app/api/preferences-token/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "your-auth-library";
import Sequenzy from "sequenzy";
const client = new Sequenzy({ apiKey: process.env.SEQUENZY_API_KEY! });
export async function POST(request: NextRequest) {
const session = await getServerSession();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const data = await client.widgets.preferences.generateToken({
email: session.user.email,
});
return NextResponse.json({ token: data.token });
} catch (error) {
console.error("Sequenzy request failed:", error);
return NextResponse.json(
{ error: "Email service request failed" },
{ status: 502 }
);
}
}
```
```python Python / Flask theme={null}
@app.route('/api/preferences-token', methods=['POST'])
def get_preferences_token():
user_email = current_user.email # Get from your auth system
response = requests.post(
'https://api.sequenzy.com/api/v1/widgets/preferences/token',
headers={
'Content-Type': 'application/json',
'X-API-Key': os.environ['SEQUENZY_API_KEY']
},
json={'email': user_email}
)
data = response.json()
return jsonify({'token': data['token']})
```
**Security**: Always call the Sequenzy API from your backend. Never expose
your API key to the frontend.
### Step 3: Frontend - Embed the Widget
Fetch the token from your backend and render the iframe:
```tsx React theme={null}
"use client";
import { useState, useEffect } from "react";
export function EmailPreferences() {
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchToken() {
const res = await fetch("/api/preferences-token", { method: "POST" });
const data = await res.json();
setToken(data.token);
setLoading(false);
}
fetchToken();
}, []);
useEffect(() => {
// Handle iframe resize messages
const handleMessage = (e: MessageEvent) => {
if (e.data.type === "sequenzy-preferences-resize") {
const iframe = document.getElementById(
"sequenzy-preferences"
) as HTMLIFrameElement;
if (iframe) iframe.style.height = e.data.height + "px";
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
if (loading) return
```
## Auto-Resize
The widget automatically sends resize messages when its content height changes. Add this listener to your page to automatically adjust the iframe height:
```javascript theme={null}
window.addEventListener("message", function (e) {
if (e.data.type === "sequenzy-preferences-resize") {
var iframe = document.getElementById("sequenzy-preferences");
if (iframe) iframe.style.height = e.data.height + "px";
}
});
```
## Widget Sections
The widget displays the following sections based on the subscriber's data:
### Email Lists
Shows public email preference categories by name. Internal list descriptions are never included. Each list has a toggle to subscribe or unsubscribe.
### Active Sequences
If the subscriber is currently in any automated email sequences, they'll see a list of active sequences with the option to cancel each one.
### Global Unsubscribe
Hosted unsubscribe links put a prominent **Unsubscribe** action before the
preference controls. It stops marketing emails immediately and leaves a
persistent confirmation with an undo option. Embedded preference widgets keep
the action below the list controls because subscribers open them to manage
preferences. Essential account emails may still be delivered.
## Styling
The widget uses a clean, minimal design that works well in most applications. A
quiet "Powered by Sequenzy" footer attribution is shown for free tier accounts
and can be removed by upgrading to a paid plan.
## Error Handling
The token endpoint returns specific error codes to help you handle edge cases:
| Status | Error Code | Description |
| - | - | - |
| 400 | `VALIDATION_ERROR` | Invalid email format |
| 401 | `UNAUTHORIZED` | Invalid or missing API key |
| 404 | `NOT_FOUND` | Subscriber not found in Sequenzy |
| 500 | `SERVER_ERROR` | Server configuration error |
If you get a 404 error, ensure the subscriber exists in Sequenzy before
requesting a token. Use the [Create
Subscriber](/api-reference/subscribers/create) endpoint to add them first.
## Best Practices
1. **Add to account settings**: Place the widget in your user's account or settings page
2. **Handle loading states**: Show a spinner while fetching the token
3. **Handle errors gracefully**: Display a friendly message if the widget fails to load
4. **Use auto-resize**: Implement the resize handler to prevent scrollbars
5. **Secure your endpoint**: Ensure only authenticated users can request tokens
## API Reference
For detailed API documentation, see:
API endpoint for generating widget tokens
Learn about subscriber management