# 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": "

Hello there!

", "fromEmail": "hello@example.com", "replyTo": "support@example.com" }' ``` ```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": "Imported April Launch", "subject": "A quick update", "status": "sent", "sentAt": "2026-05-01T14:00:00Z", "html": "

Hello there!

" }' ``` ## 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":"

Sale

"}]}' ``` ## Responses ```json 201 theme={null} { "success": true, "component": { "id": "cmp_123", "companyId": "comp_123", "name": "Promo banner", "description": null, "blocks": [{ "id": "block_1", "type": "text", "content": "

Sale

" }], "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":"

Hello

"}' ``` ## Responses ```json 201 theme={null} { "success": true, "email": { "id": "email_123", "companyId": "company_123", "name": "Welcome", "subject": "Welcome", "previewText": null, "blocks": [{ "id": "block_123", "type": "text", "variant": "html", "content": "

Hello

" }], "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":"

Updated body

"}' ``` ## Responses ```json 200 theme={null} { "success": true, "email": { "id": "email_123", "companyId": "company_123", "name": "Welcome", "subject": "Updated welcome", "previewText": null, "blocks": [{ "id": "block_123", "type": "text", "variant": "html", "content": "

Updated body

" }], "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 wait Hours to wait Minutes 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": "

Welcome

" }, { "subject": "Email 2", "html": "

Email 2

", "delay": { "days": 2 } }, { "subject": "Email 3", "html": "

Email 3

", "delay": { "days": 2 } }, { "subject": "Email 4", "html": "

Email 4

", "delay": { "days": 2 } }, { "subject": "Email 5", "html": "

Email 5

", "delay": { "days": 2 } }, { "subject": "Email 6", "html": "

Email 6

", "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" } ] }, { "subject": "Getting started guide", "delay": { "days": 1 }, "blocks": [ { "id": "heading-2", "type": "heading", "content": "Getting Started", "level": 1 }, { "id": "text-2", "type": "text", "content": "

Here is everything you need to get started...

", "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": "

Download {{event.download.name}}

" } ] }' ``` ### Event-Triggered Sequence With `{{event.*}}` ```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": "Wind Alert Follow-up", "trigger": "event_received", "eventName": "weather.wind_alert", "steps": [ { "subject": "Wind alert for {{event.city}}", "html": "

Winds may reach {{event.alert.maxSpeed}} km/h for {{event.duration.hours|a few}} hours.

" }, { "subject": "Stay safe in {{event.city}}", "delay": { "hours": 12 }, "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": "

Still thinking it over?

" } ] }, { "variantId": "var_b", "variantLabel": "B", "emailId": "email_variant_b", "subject": "Your pick is waiting", "previewText": null, "isWinner": false, "blockCount": 1, "blocks": [ { "id": "challenger", "type": "text", "content": "

Your pick is waiting

" } ] } ], "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": "

Here is one more migration resource.

" } ] }, "confirmStructuralChange": true, "emails": [ { "nodeId": "node_abc123", "subject": "Welcome to Acme", "html": "

Welcome

Here is your first step.

" } ] }' ``` 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": "

Welcome to {{COMPANY_NAME}}

", "variant": "html" } ], "variables": ["NAME", "COMPANY_NAME", "LOGIN_URL"], "email": { "id": "email_abc123", "name": "Welcome Email", "subject": "Welcome, {{NAME}}", "previewText": "Your account is ready.", "blocks": [ { "id": "block_123", "type": "text", "content": "

Welcome to {{COMPANY_NAME}}

", "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": "

Welcome to {{COMPANY_NAME}}

" }' ``` ## Responses ```json 200 theme={null} { "success": true, "transactional": { "id": "txn_abc123", "name": "Welcome Email", "slug": "welcome", "emailId": "email_abc123", "labels": ["Billing"], "enabled": true, "subject": "Welcome, {{NAME}}", "previewText": "Your account is ready.", "blocks": [ { "id": "block_123", "type": "text", "content": "

Welcome to {{COMPANY_NAME}}

", "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 Sequenzy API Keys settings with callouts showing how to create an API key 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 Sequenzy API key dialog with callouts showing purpose, preset, and permissions 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. Sequenzy notification settings with callouts showing subscriber and campaign alerts 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. Sequenzy Accounts tab with callouts showing how to create accounts from detected work domains or add one yourself 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 Sequenzy campaigns dashboard with callouts showing how to create and open a campaign 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 Sequenzy campaign template picker with callouts showing template styles and a starting point ### 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. Sequenzy campaign recipient step with callouts showing include rules, exclusions, and audience preview 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 Sequenzy campaign schedule step with callouts showing start time, delivery pace, and delivery summary * **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 Sequenzy campaign review step with callouts showing campaign details, final audience, and email preview 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 here Click 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 Sequenzy label settings with callouts showing reusable content labels 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 Sequenzy label dialog with callouts showing label name and creation 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. Sequenzy email editor with the Commerce block category open, showing cart, order, and product blocks ## 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: Sequenzy AI Agent Setup showing CLI-capable and local agent connection options ```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": "

Hello there

", "variant": "paragraph" }, { "id": "pro-offer", "type": "text", "content": "

Your Pro upgrade is ready.

", "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. Sequenzy Push settings with callouts showing the platform switcher and the Turn on web push button ## 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 Sequenzy inbox with callouts showing conversation filters and subscriber replies 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 Sequenzy reply conversation with callouts showing the thread, reply composer, and subscriber profile 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 Sequenzy sender dialog with callouts showing inbox preview and sender identity Create the visible sender identity first. Its From name and address are what recipients see in their inbox. ### Add A Reply-To Identity Sequenzy reply-to address dialog with callouts showing the identity preview and destination inbox 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 Sequenzy sequence builder with callouts showing the trigger and email steps 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? Sequenzy sequence list with callouts showing how to create and open an automation 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 Sequenzy trigger picker with callouts showing segment and event 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 Sequenzy sequence step picker with callouts showing message and branching 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). Sequenzy email sequence step with callouts showing content and compliance controls 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 Sequenzy sequence template picker with callouts showing audience filters and templates ### 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 Sequenzy live sequence test panel with callouts showing the test subscriber, accelerated timing, and live-action warning 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 Sequenzy SMS dashboard with a callout showing the SMS setup area 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. SMS campaign composer showing message content, segment and credit counts, and the phone preview ### 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. SMS campaign review showing eligible and skipped recipients, delivery timing, estimated credits, and the final phone preview 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 Sequenzy subscribers dashboard with callouts showing how to add and open contacts 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 Sequenzy subscriber profile with callouts showing activity and custom attributes 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. Sequenzy subscribers page with callouts showing manual contact creation and CSV import ### Add Manually Enter one or more email addresses or phone numbers, choose any lists and tags, and optionally enroll the new contacts in sequences. Sequenzy manual contact dialog with callouts showing contact data, audience choices, and sequence enrollment ### 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. Sequenzy CSV subscriber import with callouts showing required and optional field mapping 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 Sequenzy segment builder with callouts showing templates and precise filters 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 Sequenzy sync rules with callouts showing presets, custom rules, and rule groups 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 Sequenzy sync rule dialog with callouts showing source events and tag actions 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 Sequenzy tag settings with callouts showing custom and built-in subscriber 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 Sequenzy custom tag dialog with callouts showing tag naming and color ### 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 tracking settings with callouts showing email tracking, double opt-in, and UTM defaults 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 Sequenzy email tracking settings with callouts showing opens, clicks, and advanced controls 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 Sequenzy transactional email dashboard with callouts showing how to create and open a template 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 Sequenzy transactional API dialog with callouts showing operations and SDK examples ### 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 confirmed Thank 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 Sequenzy transactional template picker with callouts showing common template types ### 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 Sequenzy UTM settings with callouts showing automatic tagging and campaign defaults 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. Sequenzy-styled marketing email with a branded header, coral hero, feature row, testimonial, and compliance footer ## 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 Sequenzy email component library with callouts showing article and quote blocks 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. Sequenzy website tracking settings with callouts showing key creation and the install snippet 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 Sequenzy website tracking key dialog with callouts showing the key name and allowed origins 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. Sequenzy integrations page with WooCommerce highlighted under E-Commerce ## 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. Sequenzy integrations page with WordPress highlighted under Websites and Forms ## 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. Sequenzy team settings with callouts showing invitations and member access 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. Sequenzy product settings with callouts showing company data, brand assets, and compliance ## 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 Sequenzy domain details with callouts showing sender profiles and DNS records 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 Sequenzy domain dialog with callouts showing domain entry and subdomain guidance ### 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 Sequenzy email editor with callouts showing components, preview, and AI review controls 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: Sequenzy HTML import view with callouts showing file upload and pasted 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**: Sequenzy AI context settings with callouts showing brand voice and product 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 Sequenzy email preview with callouts showing client, dark mode, and preview data controls ### 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** Sequenzy transactional email dashboard with callouts showing how to create and open a template 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 Sequenzy contact import flow with callouts showing required and optional field mappings **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. Resume sending dialog showing the affected workspace, cleanup confirmation, and sender-health reset 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 Sequenzy domain dialog with callouts showing domain entry and subdomain guidance 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. Sequenzy Connect Supabase dialog with callouts showing the sync choice and the Connect Supabase button 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. Sequenzy Affonso connection dialog with callouts showing the required setup fields ## 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 Sequenzy API Keys settings with callouts showing how to create an 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. Sequenzy Chargebee connection dialog with callouts showing the required setup fields ## 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. Sequenzy Clerk connection dialog with callouts showing the required setup fields ## 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. Sequenzy Creem connection dialog with callouts showing the required setup fields ## 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 Creating Developers 1. Go to your [Creem Dashboard](https://creem.io/dashboard) 2. Navigate to **Developers** 3. Copy the API key (format: `ck_xxxxxxxxxx`) Getting your Creem API key ### 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 Finding your Creem Store ID ### 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. Custom webhook mapping with captured JSON, field dropdowns, and a custom duration property ### 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. Sequenzy Data Warehouse export view with callouts showing Export to bucket and the bucket destinations 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. Sequenzy Dodo Payments connection dialog with callouts showing the required setup fields ## 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: Sequenzy signup form templates with callouts showing how to choose and customize a form 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. Sequenzy Connect Lemon Squeezy dialog with callouts showing the store ID and API key fields ## 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. Meta audience sync dialog showing the selected segment, active audience count, ad account, frequency, and audience name 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. Sequenzy webhooks dashboard with callouts showing how to add a webhook or use the API 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 Sequenzy webhook dialog with callouts showing destination and lifecycle 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. Sequenzy Paddle connection dialog with callouts showing the required setup fields ## 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. Sequenzy Polar connection dialog with callouts showing the required setup fields ## 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. Sequenzy PostHog connection dialog with callouts showing the required setup fields ## 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** PostHog webhook destination configuration ### 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. Sequenzy Segment connection dialog with callouts showing the required setup fields ## 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. Sequenzy Integrations page showing how to find Shopify and start installation ## 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. Shopify automation settings showing browse-abandonment and price-drop timing controls ## 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. Sequenzy Shopify App connection dialog with callouts showing the required setup fields ## 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 Sequenzy integrations directory with callouts showing how to choose and connect a provider 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. Sequenzy Supabase connection dialog with callouts showing the required setup fields ## 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. Generate webhook secret in Sequenzy ### 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. Create Supabase webhook from Sequenzy 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. Sequenzy Data Warehouse settings with callouts showing Import from warehouse and the warehouse choices 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 Webflow native form capture dialog showing explicit form selection, retained fields, double opt-in, and enrollment controls 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. Sequenzy Whop connection dialog with callouts showing the required setup fields ## 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. Connected Wix integration showing site identity, status, settings, refresh, and disconnect controls ## 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 Zapier workflow showing a Typeform trigger connected to a Sequenzy Create or Update Subscriber action ## Authentication When connecting Sequenzy to Zapier, you'll need your API key: Sequenzy API Keys settings with callouts showing how to create an 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. Zapier Sequenzy action showing subscriber field mappings, tags, and sequence enrollment | 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. Sequenzy home dashboard with callouts showing the AI question box, 30-day email performance, and the Create menu ## 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 Sequenzy API key dialog with callouts showing purpose, preset, and permissions 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: "

Welcome, {{firstName}}!

Thanks for signing up.

", variables: { firstName: "John", }, }); ``` ```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", "subject": "Welcome to Our Platform!", "body": "

Welcome, {{firstName}}!

Thanks for signing up.

", "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 platform Welcome, {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 platform Welcome, {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 platform Welcome, {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. Sequenzy SMTP settings with callouts showing connection details and API key creation 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 = "

Hello!

This email was sent via SMTP.

" msg.attach(MIMEText(html, "html")) with smtplib.SMTP("smtp.sequenzy.com", 587) as server: server.starttls() server.login("api", os.environ["SEQUENZY_API_KEY"]) server.sendmail(msg["From"], msg["To"], msg.as_string()) ``` ```php PHP (PHPMailer) theme={null} use PHPMailer\PHPMailer\PHPMailer; $mail = new PHPMailer(true); $mail->isSMTP(); $mail->Host = 'smtp.sequenzy.com'; $mail->SMTPAuth = true; $mail->Username = 'api'; $mail->Password = $_ENV['SEQUENZY_API_KEY']; $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $mail->setFrom('you@your-verified-domain.com', 'Your Name'); $mail->addAddress('recipient@example.com'); $mail->isHTML(true); $mail->Subject = 'Hello from Sequenzy'; $mail->Body = '

Hello!

This email was sent via SMTP.

'; $mail->send(); ``` ```go Go (net/smtp) theme={null} package main import ( "net/smtp" "os" ) func main() { apiKey := os.Getenv("SEQUENZY_API_KEY") auth := smtp.PlainAuth("", "api", apiKey, "smtp.sequenzy.com") to := []string{"recipient@example.com"} msg := []byte("To: recipient@example.com\r\n" + "From: you@your-verified-domain.com\r\n" + "Subject: Hello from Sequenzy\r\n" + "Content-Type: text/html\r\n" + "\r\n" + "

Hello!

This email was sent via SMTP.

\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. Sequenzy popup templates with callouts showing how to choose and customize a popup The Popup Widget lets you create a signup popup inside Sequenzy and install it on any website with a hosted script. ## Quick Start Sequenzy popups list with callouts showing saved popups and popup creation 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 Sequenzy popup code dialog with callouts showing platform tabs, install code, and an AI-ready prompt 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 Sequenzy popup editor with callouts showing trigger rules and the visitor preview 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: Sequenzy signup form templates with callouts showing how to choose and customize a form ## 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 Sequenzy forms list with callouts showing saved forms and form creation Navigate to **Pages & Forms → Forms** in your [Sequenzy dashboard](https://www.sequenzy.com/dashboard) and click **New form**. ### Step 2: Customize Your Form Sequenzy form editor with callouts showing fields, live editing, and preview controls 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 Sequenzy form integration dialog with callouts showing landing page and platform installation options Add the form directly to a Sequenzy landing page, or select **Website and platform snippets** for an external site. #### Copy The Installation Snippet Sequenzy form code dialog with callouts showing install methods, copy controls, and an AI-ready prompt 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: Subscription Preferences Widget 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 Sequenzy subscription preferences settings with callouts showing the subscriber preview and widget connection 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
Loading preferences...
; if (!token) return
Unable to load preferences
; return (