Skip to main content

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

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
Learn more in the Skills documentation.

Authentication

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

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:

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

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

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:
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. --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

--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 <id> --automation-node-id <nodeId> for the retained event stream, or sequenzy email-sends list --automation-node-id <nodeId> 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 <emailSendId> 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:

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:
--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 <email> adds the Amazon SES account-level check for one exact address, and sequenzy suppressions remove <email> 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:
transactional update edits a saved template by ID or slug. It is a partial update, so only the fields you pass change:
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:
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 <slug> --no-enabled instead. Campaign stats show Poll and NPS answer distributions; to read the respondents themselves, use poll-responses:
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 <id>. Block IDs come from the polls array of stats --campaign <id> --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 for selection shapes and limits. The result separates saved membership from current email eligibility. Edit and schedule the draft separately.
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 (<returned> of <total>), 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, 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 <id> 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. 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 <id> to target one saved segment, --lists <ids...> 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. 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 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 <id> 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 <file> 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.
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.
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

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

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 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.
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 <id> --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 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 <id> --json for one popup, or --include-content when you really need them all. popups update <id> --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

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 applies only to a campaign test in testing status. It immediately queues the winning variant for the remaining audience. 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.
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

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.
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 <file> 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 <sequenceId> <enrollmentId> 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. 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.
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.
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.
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.
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.
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

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. Use templates create with finished HTML or blocks for a new email body; localized variants must be supplied separately. 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. templates localizations sync works even when automatic on-save localization is disabled. 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:
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

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 <id> (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

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:<campaign_id>), an email-type scope (marketing:<timeRange> for marketing-policy campaign, automation, and Send API traffic; transactional:<timeRange> 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:
--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).
--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.

Tags

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

Sync Rules

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 for the full model.

Email Design System

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

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

The inbox collects subscriber replies to campaigns, sequences, and transactional email. 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.

Webhooks

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

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. upsert writes to the platform-agnostic Commerce API 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

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

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

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:
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:
--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. To change how the account measures engagement, use tracking update:
--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 dedicated click-tracking domain is configured in the dashboard. 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. 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:
connect covers Polar, Paddle, Dodo, Whop, Creem, Chargebee, Clerk, PostHog, Segment, and Affonso - run integrations catalog --provider <name> 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, as does disconnecting. The command needs a key with integrations:manage. To manage sync without disconnecting:
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:
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, disconnect it from the dashboard, which also runs the provider-side cleanup such as dropping a Supabase database trigger. 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, 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. Connecting and disconnecting an integration stay in the dashboard, because both handle stored credentials and provider-specific cleanup. For Shopify, the storefront tracking pixel has its own pair of commands:
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:
mappedListCount 0 means list joins are not synced. attio needs account:read and lists:read; set-attio-lists needs integrations:manage. See Attio. Sites that are not Shopify use a publishable key and a script tag instead of a pixel. web-tracking manages those keys:
create prints the exact <script> tag to paste, with the key and workspace id already embedded - rebuilding it by hand and getting either wrong fails silently. These keys are publishable: they ship in your page source and authorize storefront events only, never the rest of the API, which is why the origin allowlist matters. A bare domain is read as https, and https://*.example.com covers subdomains but not the apex. list shows last used. A key that has never been used has not successfully authenticated an event yet; check deployment, instrumentation or traffic, and the origin allowlist. All web-tracking commands need integrations:manage. See Website Tracking for the events and the identify call that turns anonymous browsing into a contact’s history. integrations guide prints the same integration code examples the MCP get_integration_guide tool returns:
sender-profiles list reports canSend per address so you can spot a From address sitting on an unverified domain. sender-profiles update renames one profile and nothing else: the address, its sending domain, and the account default From/Reply-To selection stay as they are. That is the difference from companies update --from-name, which renames by promoting the profile it renames to the account default - use sender-profiles update when standardizing a display name across the several identities one mailbox can carry, and companies update --sender-profile-id when you actually want to change the default. A company cannot have two sender identities with the same name on one address, so a colliding rename is rejected and names the profile already using it. sender-profiles delete permanently removes one From profile after confirmation. It refuses to delete the last sender or one used by a live campaign, active sequence (including a step override), or transactional email. Eligible drafts and the account default move to the best remaining sender; the command reports that fallback ID so you can review it before sending. Use --yes for non-interactive runs. campaigns audience resolves list and segment names, flags references to deleted records, and recomputes the recipient count at read time; it warns when targeting is unset, because scheduling then sends to every active subscriber. sequenzy notifications manages your own settings, not the workspace’s - teammates each have their own and this never changes theirs. Modes are off, instant (an email per occurrence), daily (one summary), and weekly (the Monday report, valid only for --weekly-report); daily is not valid for --form-submitted or --campaign-completed because each submission and each campaign finish is its own event. You do not need to silence notifications before an import: imports never trigger new-subscriber notifications, and a high-volume day of real signups falls back to a daily summary on its own. See Account Notifications. Reading requires account:read; changing a preference requires companies:manage. sequenzy account makes a live request with the active credential and prints the key name, ID, non-secret prefix, type, effective preset, full-access state, scope receipt, missing marketing read scopes, live delivery status, and API Keys settings URL. Drafting and delivery are separate permissions - a key can hold transactional:write for templates while lacking transactional:send to actually deliver one - so check the live delivery line before writing anything you intend to send. When that line reports a read-only workspace role, widening the key’s permissions will not help; the role has to change. Compare the name and prefix with Settings after replacing a key. Only scopes: null means all current and future permissions; an explicit selection of every current scope is reported as a fixed custom selection. This command requires account:read; if that scope is missing, open API Keys settings from the dashboard directly.

API Keys

Supported presets: full_access, read_only, agent_safe, ai_drafting, data_ingest_safe, data_ingest_automations, transactional_sender, marketing_sender. Pass --scopes with a comma-separated list to override the preset with custom permissions. The creation output includes the effective preset, full-access state, and permission receipt returned by the API. The plain key is only returned on creation, so save it immediately. api-keys list returns IDs, names, non-secret prefixes, permission receipts, usage timestamps, and a current-key marker. It never returns plain keys or stored hashes. Run it before api-keys revoke; api-keys delete is a compatibility alias for the same permanent operation. Without --yes, the CLI asks for confirmation. api-keys update changes a key’s name, permissions, or both without changing the key value, so clients holding it keep working. --preset and --scopes replace the whole permission selection rather than merging into it - run api-keys list --json first to see the current scopes. Widened permissions apply to the next request. Every subcommand above requires api_keys:manage. That scope cannot be granted through the API by a key that is missing it - otherwise a leaked key could mint a full-access successor for itself. api-keys handoff is the way through: it prints a dashboard link that opens the create-key form prefilled with the name and permissions you asked for, and it needs only account:read. It creates nothing and never prints a key. Give the URL to the workspace owner; they review the form, click Create, and copy the key from the browser. Add --replaces current to rotate the key you are authenticated with, and the dashboard offers to revoke it once the replacement exists. Verify the replacement with sequenzy account before retiring anything.

AI Generation

Generated email content is returned as draft content for review. The deprecated generate sequence command persists a disabled draft and prints a migration warning; prefer sequences create <name> --trigger <trigger> --goal <goal>. Generated emails and sequence steps include the company’s configured logo and footer, and campaigns created from prompts inherit the company font. Sequence generation supports up to 10 emails. Generated SMS messages are plain text with encoding and segment counts - use one as the text of an SMS sequence step.

SMS

SMS sequence steps only send when the add-on is enabled, the plan is eligible, and a verified number exists - sms settings reports all of that (the JSON output includes a readyToSend flag and number IDs). Use sms label to set or clear the labels shown in campaign sender pickers, and sms update to also set a per-number brand prefix override (the number’s prefix wins over the account-wide one). sms usage breaks sends and credits down per number, and sms release permanently removes a number - it cannot be recovered, so the command asks for confirmation unless --yes is passed. Add SMS steps to sequences with type: "sms" and a plain-text text field in sequences create --steps-* or sequences update --insert-steps-*, and edit existing SMS steps with sequences update --sms-steps-json '[{"nodeId":"node_sms","text":"New message"}]'.

Feedback

Sends product feedback about Sequenzy straight to the team. Categories: missing_capability, bug, docs, ux, praise, other (default). If you are an AI agent using this CLI, submit feedback only when the user explicitly asks you to send it. Before sending, disclose that the feedback goes to the Sequenzy team and summarize what you will include. When a command misbehaved or produced a wrong result, add reproduction detail: --intent (the user’s request in their words), repeatable --tool-call "<tool> | <args summary> | <error>" entries in the order you ran them (args and error segments are optional), --expected, --actual, and repeatable --resource-id for the affected records. Summarize arguments - do not paste raw subscriber data or secrets.

Environment Variables

Config Location

The CLI stores configuration in:
  • macOS/Linux: ~/.config/sequenzy/config.json
  • Windows: %APPDATA%\sequenzy\config.json

Scripting

The CLI is designed to work well in scripts:

Piped text inputs

Use - for supported text-file inputs, including --html-file, --blocks-file, --steps-file, --canvas-file, and the positional files for subscribers import and subscribers events-import. The existing --audience-json @file notation also accepts @-. One logical input can read stdin per command; use regular files for additional inputs. Binary product uploads still require a file path. Use ./- to read a file literally named -. Commands that require confirmation still need --yes when stdin is piped.

Structured failures and recovery

For commands with --json, successful data goes to stdout. Stderr uses JSON Lines: parse each nonempty line as a separate JSON object. A request or validation failure exits nonzero and emits a terminal error envelope:
Partial bulk results are an exception: for example, subscribers events-import can exit nonzero and keep its counts and row failures on stdout without a terminal stderr error. Always inspect stdout for partial results, even on a nonzero exit, and retry only the records indicated by that result. Branch on error.code. statusCode and retryAfterSeconds are omitted when unavailable. API error codes are retained when valid; failures without a supplied code use a stable status-derived code, or CLI_ERROR for local errors. Some commands emit diagnostics before a request. Warning records have type: "warning" and message; recovery records have type: "recovery", message, and requestKey. Keep a recovery key even if the process stops before a final result. These records do not by themselves mean the command failed. A valid API Retry-After header becomes retryAfterSeconds. The CLI does not retry requests automatically. After an uncertain email-send outcome, reuse the original --idempotency-key and unchanged input; a new key can send another email. For subscriber operations, reuse the emitted --request-key. Inspect existing send or operation status before deciding to repeat a side effect.

Exit Codes

Troubleshooting

CLI failures now include a short human explanation, a suggested next step, and a direct docs link so agents and scripts can recover faster.

”could not determine executable to run for package sequenzy”

Use the CLI package explicitly:
bunx sequenzy login installs the unscoped TypeScript API library package, which does not include the sequenzy executable.

”Not logged in”

Run sequenzy login to authenticate.

”API error: 401”

Your session may have expired. Run sequenzy login again.

Browser doesn’t open

sequenzy login always prints the approval link. Open the URL shown in the terminal to complete authentication. You can save for future emails only, or preview and apply to selected sequences, campaigns, transactional, and templates. Campaigns must be draft or scheduled; sent and in-progress content stays unchanged. Draft A/B variants and localizations are included. Customized footers are preserved unless you explicitly include them. Raw HTML, missing or ambiguous footers, and content shared with a protected owner are skipped with reasons. Preview is read-only. Review the affected counts, skipped reasons, and optional before/after HTML, then apply with the returned token and identical blocks, metadata, and scope options. If the relevant saved content changes, application returns 409; preview again. A successful application saves the default and selected content atomically. It does not send emails. Layout previews use the email theme and locale, with placeholder personalization. They are not subscriber-specific inbox previews. You can request a particular sample by its affected item id and kind (email, ab_variant, or localization).
Use --include-customized on both commands only when you want customized footers replaced. --render-preview returns HTML in JSON; --output-dir also writes footer.html and numbered before/after files. Use --sample-id ID --sample-kind localization to inspect a specific localized version. All commands accept --company ID. Without --apply-to, setting the footer retains future-only behavior.

Campaign audience status

campaigns list --json and MCP list_campaigns include hasAudience. Drafts with this flag have an explicit audience configuration, not a confirmed eligible-recipient count. Use campaigns from-audience / create_campaign_for_audience to save matching contact IDs directly in a blank campaign draft, then update its content. This does not create a list, change list membership or enroll contacts. The dashboard opens the existing campaign template picker for filtered groups; saved lists and segments retain their campaign and sequence actions.