> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sequenzy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Tool Reference

> Every tool, resource and workflow the Sequenzy MCP server gives ChatGPT, Claude, Cursor, Codex and other AI assistants

This page lists everything your AI assistant can do once Sequenzy is
[connected over MCP](/concepts/mcp). You don't need to call these tools
yourself. Ask in plain language, and your assistant picks the right tools.

What a tool may change depends on the permissions you approved when you
connected. See [MCP permissions and privacy](/concepts/mcp-permissions) to
check or widen them.

## How tools behave

Tools reject arguments they do not declare instead of ignoring them. An
argument a tool never supported would otherwise come back as a result that
silently does not match the request - an invented `filters` on a search, for
example, returning the unfiltered list. The error names the unsupported
arguments, lists the supported ones, and points at the supported way to express
the same request where one exists.

### Errors and recovery

Tool failures retain `isError: true` and readable error text, and also return
`structuredContent` with `{ success: false, error: { code, message, howToFix,
docsUrl } }`. HTTP errors include `statusCode`; a valid API `Retry-After` header
adds `retryAfterSeconds`. These fields are omitted when unavailable. The OpenAI
profile uses safe recovery descriptions and known codes or status-derived
fallbacks, omitting raw backend diagnostics.

Use the error code and recovery guidance to choose your next step. The client
does not retry automatically. After an uncertain `send_email` outcome, reuse the
original `idempotencyKey` with unchanged input and inspect `emailSendId` when
available. A retry hint alone does not mean repeating a write is safe.

Successful results keep their existing shapes. Recoverable partial subscriber
event imports retain their counts and row failures without `isError`; inspect
those rows before retrying failed records.

### Duplicate resources

If a tool call would create a duplicate segment name or sending domain, the error includes a stable `code`, an agent-friendly `description`, a concrete `resolution`, and a `docsUrl`. For segments, call `list_segments` and reuse the existing segment ID or choose a different name. For websites, call `list_websites`; if the domain is not listed for the selected company, it belongs to another company or account and must be removed, reassigned, or replaced with a different sending domain.

## Working across multiple workspaces

Call `get_account` to list the companies your connection can access and your
role in each one. Then call `select_company`, or pass `companyId` on a tool that
supports it, before working with company data.

The selection sticks until you change it: the local server keeps it for as long
as the MCP process runs, and a hosted connection remembers it for up to 30 days
per API key. A `companyId` passed on an individual call applies to that call
only. If the selected company is later deleted, or your access to it is removed,
`get_account` clears the selection and falls back to a company you can still
reach, so you are never locked out by a stale choice.

Changing the selected company does not create a separate subscription. Companies
owned by the same user share that owner's core email plan, monthly email usage,
and email credits. A company you joined uses its own owner's billing pool.
Sender-health pauses and the daily sending cap remain company-specific, while a
serious owner-account ban affects every company that owner controls.

See [Workspaces, Billing, and Shared Limits](/concepts/workspaces-and-billing)
for invoice, add-on, quota, and reputation details.

## Subscribers

* `add_subscriber` - Add a new subscriber by email, or by phone alone for a phone-only (SMS) contact; its status is creation-only, so use `update_subscriber` for an existing contact. The `subscribers:write` scope covers a nonempty `listIds` array in this call; omit it to let a new contact follow the workspace default lists while an existing contact keeps its memberships, or pass `[]` to join no list. The data-ingest presets support explicit lists in this call. Imports into explicit lists and dedicated list membership tools still require `lists:write`. Pass `createdAt` when importing from another platform to keep the contact's original signup date, so date-relative segments are right immediately; an existing contact's date only ever moves earlier, and supplying it skips sequence enrollment by default. Pass `enrollInSequences: true` to override that default with an API key that has `automations:trigger`
* `create_subscriber_import` - Queue up to 5,000 full CRM subscriber records in one call, including names, external IDs, phone numbers, tags, lists, statuses, custom attributes, and a per-record `createdAt` signup date. A nonempty `listIds` array requires `lists:write`. Workspace-default opt-in can require `automations:trigger` for active contacts, and double opt-in always requires it. Pass `idempotencyKey` from retry loops so a resent request returns the already-queued import instead of creating a second batch
* `get_subscriber_import` - Check import progress and row-level outcome counts. Every excluded row is explained: `skippedReasons` sums to `skippedCount` and `failedReasons` sums to `failedCount`, so a migration can be proven complete without inspecting individual contacts
* `update_subscriber` - Update profile fields, attributes, tags, or global status; the legacy `attributes` spelling merges keys into the existing custom-attribute map, while the REST-compatible `customAttributes` spelling replaces the public map and removes unspecified keys. `status: "unsubscribed"` suppresses future marketing while preserving history. `firstName`, `lastName`, `timezone`, and `phone` are native profile fields, so set them here rather than as custom attributes - `timezone` takes an IANA identifier (e.g. `America/New_York`) and enables recipient-local campaign delivery - a national-format number needs `phoneCountry` (for example `IT`), and changing a number resets SMS consent unless you send `smsConsent` in the same call
* `remove_subscriber` - Unsubscribe while preserving history, or permanently delete only with `hardDelete: true`
* `get_subscriber` - Get subscriber details
* `list_subscriber_attributes` - List the custom attribute names in use across the account, each with `valueType` (`string`, `number`, or `boolean`), `isArray`, `mixedTypes`, an example `sampleValue`, and `sampledContacts`. When `mixedTypes` is true, contacts hold different types for the key and `valueType` is `string`. Call it before `add_subscriber` or `update_subscriber` to reuse existing names and send matching types, for example a zip code as the string `"02134"`. Pass `includeNested: true` for paths such as `profile.tier`
* `search_subscribers` - Search by free-text query, tags, lists, segments, or a custom attribute. Omit `limit` and the tool walks every page for you and returns all matches in one call. To read a large audience in chunks instead, pass `limit` and follow `pagination.nextCursor` until `pagination.hasMore` is false; `offset` and `page` also work below 1,000,000 skipped matches and are what `pagination.nextOffset` is for when `nextCursor` is null. Use cursors for deeper audiences. Filter on a custom attribute with `attribute` plus `attributeValue` and an optional `attributeOperator` (`is`, `contains`, `gt`, `gte`, `lt`, `lte`, or `is_not_empty`); `is_not_empty` needs no value and matches every contact that has the attribute set. Attribute filters cannot be combined with `segmentId` - create a segment for compound or `is_not`/`is_empty` attribute rules. Each subscriber carries `unsubscribedAt`, the date the contact opted out; use it rather than `updatedAt`, which any later tag or attribute write moves. Narrow to a window of opt-outs with `unsubscribedAfter` and `unsubscribedBefore`, which skip contacts that carry no opt-out date - active contacts and imports with no known date. Bare dates use UTC midnight; datetimes must include `Z` or an explicit offset
* `trigger_subscriber_event` - Emit a custom event for one subscriber exactly as an integration would. This is the supported way to exercise event triggers, matching-field idempotency, branch conditions, and stop conditions end to end without waiting for real traffic. Pass `occurredAt` to backfill an event that already happened: more than an hour in the past it is recorded as history, counted by segments and the timeline but running no sequences, sync rules, waiting steps or webhooks. Never use it to test a live automation, since the side effects you are testing will not run. Pass your own `eventId` to make a retry safe: a repeated live event returns the existing `event` with `duplicate: true`, while a repeated historical event stays in the `events` array and increments `duplicates`
* `trigger_subscriber_events` - Emit several events for one subscriber in order, up to 500 per call. When every event carries an `occurredAt` more than an hour old the batch imports as history in one idempotent write; add your own `eventId` so a re-run writes nothing new

For an account-qualified contact event, pass `account: "org_123"` or `account: { externalId: "org_123", role: "owner" }` to `trigger_subscriber_event`. Object references also accept `name`, `domain`, and `attributes`. The contact is attached before recording the event, which carries `event.account.*` context. Until the workspace has Accounts on, `account` is ignored and the result includes `accountIgnored`; call `upsert_account` first. The batch tool retains its existing contract; use the single-event tool when attaching an account.

* `import_subscriber_events` - Record up to 25 events for many subscribers, each carrying `email` or an existing contact's `externalId` plus a source-owned `eventId`. Contacts whose rows are all more than an hour old import silently as history; any recent row makes that contact's whole group live. Retrying reuses one receipt and repairs downstream work idempotently
* `bulk_add_subscriber_tags` / `bulk_remove_subscriber_tags` - Add or remove tags across up to 500 existing subscribers identified by `emails`, `externalIds`, or `subscriberIds`. Built for reconciling derived-tag backlogs: unknown identifiers come back in `notFound` instead of creating contacts, and tag automations stay off unless you pass `triggerAutomations: true`

## Accounts

Accounts are the companies, workspaces or teams your contacts belong to. They are unrelated to `get_account`, which returns your own Sequenzy account. Account attributes fan out to every member as `account.<name>` for segments and `{{account.<name>}}` merge tags; members have a role (`owner`, `admin`, `member`). See [Accounts](/concepts/accounts).

* `list_accounts` - List accounts with search across name, external ID and domain, plus paging and sorting by `updatedAt`, `createdAt`, `name`, `memberCount` or `lastEventAt`
* `get_account_by_external_id` - Get one account by the customer-owned organization id, with up to 100 members and their roles (owners first)
* `upsert_account` - Create or update an account by external ID. Attributes merge into the stored map (`null` deletes a key; `replaceAttributes: true` replaces everything). Optional `members` are added in the same call
* `delete_account` - Delete an account and its memberships; contacts are kept and their `account.*` values cleared
* `list_account_members` - List members with roles
* `add_account_member` / `remove_account_member` - Attach or detach a contact by `email` or `subscriberExternalId`. Adding a new email creates the contact; re-adding with a `role` updates it, without one keeps the current role
* `trigger_account_event` - Record an event on the account (for example `trial_ending`) and deliver it to `owners` (default, falling back to admins), `admins`, `all` or `none`. Each recipient gets a normal contact event carrying `event.account.*`, so sequences, sync rules and matching-field enrollment on `account.externalId` (once per account) all work. Pass `eventId` for idempotent retries and `occurredAt` (ISO 8601) to preserve the original event time
* `list_account_events` - Recent account timeline events, newest first
* `list_account_suggestions` - Read-only: work email domains several contacts share (`minContacts`, default 3; `limit`, default 25), with up to 10 `sampleEmails`, skipping personal and disposable providers, your sending domains, domains where a contact already belongs to an account and domains an account already uses
* `accept_account_suggestions` - Create accounts for up to 25 `domains` the user confirmed. Each domain becomes an account (external ID and domain set to the domain) with its unassigned contacts as members, or its contacts join the one account already using that domain. Results are `created`, `updated` or `skipped` (`no_contacts`, `multiple_accounts`, `already_in_account`); retries are safe, and no sync rules or segment-entered sequences run
* `detect_account_organization_ids` - Read-only: event properties and contact attributes that hold your organization ID, such as `workspaceId`, with how many organizations each has and the matching `nameKey`
* `preview_accounts_from_organization_id` - Read-only: the biggest accounts a `propertyKey` would create, each with the `name` it would get (from `nameKey`, otherwise its contacts' shared work email domain), `domain`, `contactCount` and `sampleEmails`. Show it to the user before creating anything
* `create_accounts_from_organization_id` - Start a background run that creates one account per organization ID and adds its contacts as members, after the user confirms the preview. Returns `jobId` and `alreadyRunning`; nothing is sent, existing names and roles are kept, and reruns are safe
* `get_account_organization_id_job` - Check that run: `queued`, `running`, `completed` with counts, or `failed` with the error

## Lists

* `list_lists` - List subscriber lists. 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
* `create_list` - Create a subscriber list
* `update_list` - Update a list's name, description, or privacy
* `delete_list` - Permanently delete a list and all of its memberships; subscribers themselves are kept
* `add_subscribers_to_list` - Bulk add up to 500 subscribers to a list from an email array
* `remove_subscribers_from_list` - Remove up to 500 subscribers from a list by email; subscribers stay in your audience and unmatched emails are returned in `notFound`

## Campaigns

Use `create_campaign_for_audience` to create a blank draft from selected contacts, contact filters, or an email activity drilldown. Preserve the complete group and time window across pages. Review `selectedCount` and `eligibleCount`, then use the existing campaign tools to edit, inspect, clear or schedule the draft. See [selection inputs and limits](/api-reference/campaigns/from-audience).

* `list_campaigns` - List campaigns with status, label, `limit`, and `offset` filters, including reviewer feedback in `rejectionComment` for rejected campaigns. Each item carries delivery pacing (`sendTimeOptimization`, `sendTimeWindowHours`, `spreadOverHours`, `sendInRecipientTimezone`, `scheduledTimezone`) so a company-wide Send Time Optimization audit does not need one `get_campaign` call each. STO is campaign-only; sequences use `sendingWindow`. Results are paginated: the default page size is 50 and `limit` is capped at 100. Read the returned `pagination` object (`limit`, `offset`, `count`, `total`, `hasMore`) and keep paging with `offset` while `hasMore` is `true` - one call does not necessarily return every campaign
* `get_campaign` - Get campaign details and stats, including the saved audience in `targetLists` (null when targeting is still unset), the linked email body in `emailId` (the same record the template tools read; reusable as `templateId` in `create_campaign`, null for SMS campaigns), and reviewer feedback in `rejectionComment` for rejected campaigns. `rejectionComment` stays `null` while a campaign is still in `waiting_approval`. Finished campaigns also report recorded pacing through `spreadOverHours`, `sendTimeOptimization`, and `sendTimeWindowHours`, plus the recipient cap in `maxRecipients` (`null` for the whole audience), alongside `sentAt` - 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. Imported campaigns may not include source-provider pacing metadata, so absent pacing fields do not prove delivery happened all at once. Note that `computedLists` is email personalization (product lists rendered inside the email), not audience targeting
* `get_campaign_audience` - Resolve exactly who a campaign will reach: targeting kind, resolved list and segment names (with a `missing` flag for deleted references), filters, include/exclude rules, a plain-language summary, and a live recipient count. `isUnset: true` means scheduling would send to every active subscriber
* `list_campaign_goals` / `create_campaign_goal` / `update_campaign_goal` / `delete_campaign_goal` - Manage persisted event, subscriber-attribute, or tag-applied conversion goals attached to one campaign. These appear on the campaign report as named conversion steps after sent/opened/clicked
* `create_campaign` - Create a campaign from a prompt, raw HTML, or Sequenzy blocks; native blocks include [Poll and NPS surveys](/concepts/polls). Prompt generation saves the generated subject, preview text, company logo/footer, and uses the company font. Pass `emailPreset: "branded" | "minimal"` to set Style > Format for native blocks; this is separate from prompt-generation `style`, cannot be combined with `html`, and is unsupported when the whole email is one raw HTML block. Applying minimal removes standalone logos; restoring branded generates a new logo unless the authored logo block is sent again. Defaults to draft, with `status: "sent"` available for archived/imported sent campaigns. Pass `targetLists` (or the `segmentId`/`listIds` shorthands) to save the audience on the draft, or omit them and choose it in `update_campaign` or `schedule_campaign`. Set the sending identity either with `senderProfileId`/`replyProfileId` from `list_sender_profiles` - each already carries its own display name, so send them without `fromEmail`/`fromName` and `replyTo`/`replyToName` - or with `fromEmail`/`replyTo` plus optional `fromName`/`replyToName` to name a profile Sequenzy creates for that address
* `update_campaign` - Edit a draft, including reply-to settings, raw HTML, or Sequenzy blocks such as Poll and NPS surveys. Pass `emailPreset` to change Style > Format without rewriting copy, subject to the same native-block and logo-loss rules as `create_campaign`. Accepts `targetLists` (or the `segmentId`/`listIds` shorthands) to retarget the draft, or `targetLists: null` to clear saved targeting. Sending identity follows the same rule as `create_campaign`: a profile ID replaces both the address and the display name. Set `sendTimeOptimization` and `sendTimeWindowHours` (1-24, default 12) on the draft; they are used when the campaign is later scheduled unless `schedule_campaign` overrides them. STO is campaign-only - there is no company or sequence toggle. `maxRecipients` (1-10,000,000, or `null` to clear) caps the send to the first N matching subscribers by subscriber id
* `schedule_campaign` - Schedule a draft or already scheduled campaign; pass `targetLists` or the `listIds` shorthand to pick the audience at schedule time (omitting both reuses saved targeting, or defaults to every active subscriber), and `recurringInterval: "weekly"` or `"monthly"` to repeat it automatically, re-evaluating audience membership at every run. Pass `sendTimeOptimization: true` to deliver each recipient at their predicted best hour within `sendTimeWindowHours` of `scheduledAt` (default 12h, max 24); this is campaign-only and is not available on sequences. `spreadOverHours` takes precedence and turns STO off. `maxRecipients` sends to at most that many audience members (omit to keep the draft's saved cap, `null` to clear it; recipients already reached count against it on resume). Pass `sendInRecipientTimezone: true` with `scheduledTimezone` (the IANA zone the `scheduledAt` wall time refers to) to deliver at that wall-clock time in each recipient's own stored timezone; contacts without a timezone receive the campaign at `scheduledAt` itself. The returned `campaign.status` is either `scheduled` or `waiting_approval`. `waiting_approval` means the campaign was held for [safety review](/concepts/campaigns#safety-review) and nothing sends until a reviewer approves it - most common on new accounts and recently registered sending domains. It is a successful outcome, not an error, and scheduling again does not clear it; check the campaign with `get_campaign` instead
* `unschedule_campaign` - Remove a scheduled send and recurrence, returning the campaign to an editable draft that can be scheduled again
* `send_test_email` - Send test to single address
* `render_email` - Render a campaign, sequence email step, or template to the exact email-safe HTML that would be sent, for embedding a visual preview in an external dashboard or builder, or for checking merge tags before anyone is enrolled. Pass exactly one of `campaignId`, `sequenceId` plus `nodeId`, or `templateId`. `unresolvedMergeTags` lists every tag that did not resolve, with reason `unknown` (nothing provides that name, so it stays empty for every recipient) or `no_value` (recognized or unverifiable, but blank for this contact) - the HTML alone cannot tell them apart. An unknown name is reported even when a `default` filter supplied text in its place, because that fallback then reaches every recipient while the HTML looks correctly personalized. A name is only called `unknown` when the render had a source to check it against. Without the contact's attributes nothing is checkable - a bare `{{plan}}` reads the same attribute map as `{{subscriber.plan}}` - so pass a stored `subscriberId` or an inline `subscriber` with `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. Rendering a transactional email needs `variables`, since its tags come from the `variables` of each send call and carry no prefix marking them. Otherwise they report `no_value` rather than being called typos. An optional attribute this contact never had set is kept out of `unknown` by checking the names other contacts in the account carry, which needs the `subscribers:read` scope. A sequence step downstream of a create-discount step renders `{{discount.*}}` from that step's real configuration with a placeholder `TEST-CODE` for the code; on a step some paths reach without running that discount step, and on a standalone template, `{{discount.*}}` reports `no_value` instead of being called a typo. `unevaluatedConditions` does the same job for conditional blocks: a condition on stored subscriber state (`tag`, `segment`, `list`, `status`, `event`, purchases, engagement) can only be evaluated for a stored `subscriberId`, or for a `tag` condition an inline `subscriber` carrying `tags`, and anything else renders as false - indistinguishable in the HTML from a contact who genuinely does not match. Read-only: it never sends or modifies anything
* `cancel_campaign` - Cancel a scheduled, paused, or sending campaign; remaining emails are not sent and the campaign cannot be restarted
* `pause_campaign` - Pause a campaign that is currently sending
* `resume_campaign` - Resume a paused campaign, optionally spreading the remaining delivery over 1-72 hours with `spreadOverHours`
* `duplicate_campaign` - Duplicate a campaign as a new draft. `mode: "campaign"` (default) copies the campaign and its email, `"ab_test"` also copies the A/B test with all variants, and `"variant"` copies a single variant's content (requires `variantId`)
* `resend_campaign_to_non_openers` - Create a draft that resends a sent campaign to everyone in the same audience who didn't open it. Available 6 hours after the campaign finishes sending, and never for imported already-sent campaigns (Sequenzy has no opens for a send it did not deliver); returns the draft and an estimate of how many subscribers haven't opened the original. The draft must be scheduled or sent separately 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.
* `share_campaign` - Create (or fetch) the campaign's public "view in browser" link. The hosted page renders an anonymized copy - sample contact, inert unsubscribe link, no open/click tracking - so the URL is safe to forward to anyone. Idempotent: an already-active link is returned with `created: false` instead of being rotated. `get_campaign` reports the current link as `shareUrl`
* `unshare_campaign` - Revoke the public link; the shared URL returns 404 immediately, and sharing again later mints a different URL
* `delete_campaign` - Permanently delete a campaign; sending, scheduled, or paused campaigns must be cancelled with `cancel_campaign` first

## Saved Forms

* `list_forms` - List saved forms, audience settings, and public action URLs
* `create_form` - Create and publish a form scoped to one or more list IDs, optionally with brand `theme` overrides
* `update_form` - Rename a form, retarget its audience, edit its copy, restyle its theme, or replace its content blocks (read the current blocks via `list_forms` first)
* `get_form_embed` - Get the action URL, hosted JavaScript, native form, and fetch example

Typed custom fields (select, textarea, hidden, and so on) are added by
replacing `blocks` with `update_form`. The
[Update Saved Form reference](/api-reference/widgets/update-saved-form)
documents every `form-field` property, including the `mapsTo` targets, choice
`options`, and hidden field behavior. A `group` block can arrange recursive
`children` as a `stack`, `row`, `grid`, or image `overlay`; groups may nest up
to three levels and responsive rows/grids collapse to one column on small
screens. Overlay requires exactly one direct image. Use `overlayColor`,
`overlayShade` (0-100), and `overlayPosition` (`top`, `center`, or `bottom`) to
control the image treatment. `gap` spaces the foreground children without
moving the background image; nested images only enable Overlay for their own
subgroup.

For a static Astro, Hugo, Jekyll, Cloudflare Pages, Netlify, or GitHub Pages
site, start with `list_forms`, call `create_form` if needed, then use
`get_form_embed`. The returned browser code never includes a Sequenzy API key;
the opaque form ID selects the server-managed lists, tags, duplicate behavior,
and success action.

## Popups

Popups are the on-site overlay capture surface: one script tag on the site, with
the trigger, page targeting, schedule, and display frequency owned by Sequenzy.
Use saved forms instead when the signup box should sit inline in the page.

* `list_popups` - List popups with their status, counts, and engagement rates. Content blocks are omitted unless you pass `includeContent`, so listing a workspace stays cheap
* `get_popup` - Get one popup's complete content blocks, trigger, targeting, schedule, frequency, and theme
* `create_popup` - Create a popup from a starting template and get its embed script; published by default
* `update_popup` - Rename, publish or unpublish via `status`, retarget, retime, edit copy, restyle, or replace content blocks
* `duplicate_popup` - Copy a popup into a new draft with its own counts, leaving the original live
* `get_popup_embed` - Get the script URL plus HTML, React/Next.js, WordPress, and Shopify snippets
* `delete_popup` - Permanently delete a popup and its view and conversion counts

`create_popup` accepts a `template` for the 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`), then `update_popup` refines it.
Unlike saved forms, `listIds` is optional: omit it and the popup captures into
every list, matching the dashboard default.
Popup block arrays support the same recursive `group` layout as saved forms.

The `trigger`, `targeting`, `schedule`, `frequency`, and `visual` objects are
merged key by key, so setting `trigger.delaySeconds` keeps the rest of the
trigger. To stop a popup showing without losing its stats or invalidating its
embed script, set `status: "draft"` rather than deleting it.

Every popup carries a `stats` object with the full funnel: `views` (shown),
`starts` (began filling in), and `conversions` (submitted), plus `startRate`,
`conversionRate`, and `completionRate`. A rate is `null` rather than `0` when
its denominator is zero, so a popup nobody has seen yet does not read as a
popup nobody converted on. To trial a change against a performer, use
`duplicate_popup` and publish the copy rather than editing the original.

## Landing Pages

* `list_landing_pages` - List landing pages with status, metrics, content, and URLs
* `get_landing_page` - Get landing page details, editor content, lifetime metrics, public URLs, and a signed `previewUrl` that works for drafts. Use `get_landing_page_stats` for unique visits, referrers, and crawler hits
* `get_landing_page_stats` - Mailchimp-style report: visits, unique visits, clicks, subscribes, conversion rate, daily histogram, referrers, UTM sources, and crawler hits. Preview URLs and the editor are never counted
* `render_landing_page` - Return a visitor-facing preview URL for a landing page without publishing it. Drafts keep `publicUrl` null; open `previewUrl` to check layout, copy, and `#form` anchors. The preview is not indexed and draft forms do not collect contacts
* `create_landing_page` - Create a draft landing page from default template content or supplied JSON
* `update_landing_page` - Edit landing page name, slug, or full editor-compatible content
* `publish_landing_page` - Publish a landing page, optionally saving name, slug, or content first
* `unpublish_landing_page` - Return a landing page to draft, optionally saving edits first
* `duplicate_landing_page` - Copy a landing page into a new draft with its own slug and stats
* `delete_landing_page` - Delete a landing page
* `connect_landing_page_domain` - Connect a workspace domain, or provide `landingPageId` for a hostname dedicated to one page
* `update_landing_page_domain_settings` - Update or verify workspace/page domain settings; provide the same `landingPageId` for a dedicated page domain
* `remove_landing_page_domain` - Remove a dedicated page domain while preserving its workspace and Sequenzy fallback URLs

Landing page content uses Sequenzy's editor-compatible JSON schema with `version`, `template`, `seo`, `theme`, and `blocks`. Blocks render in slot order - `top` (full-width band above the hero), `hero`, `form`, `body`, `footer` - so put banner images or announcement bars in the `top` slot. A `video` block takes a pasted YouTube URL in `url`; other providers and direct video files are not supported. The `seo` object also accepts `faviconUrl` (falls back to the company logo) and `hideFromSearchEngines` (adds a `noindex, nofollow` tag for private pages). The API validates CTA, pricing, footer, and form redirect URLs before saving or publishing. Button and pricing CTA URLs also accept in-page anchors: `#form` scrolls to the page's single form block, `#section-<sectionId>` and `#block-<blockId>` scroll to any section or block, and `#top` returns to the top - use these for repeated CTAs on a one-form page instead of adding a second form. The `theme` object accepts `sectionAnimation` (`none`, `fade`, `slide-up`, `zoom-in`) and `sectionAnimationSpeed` (`slow`, `normal`, `fast`) for a scroll reveal on the published page. Custom landing page subdomains require a CNAME record pointing to `pages.sequenzydns.com`; root domains require an A record pointing to `76.76.21.21` (the `www` host also redirects to the root when its CNAME points to `pages.sequenzydns.com`). Call `update_landing_page_domain_settings` with `verify: true` after DNS changes propagate. A dedicated page domain opens at its hostname root and never exposes sibling pages; existing workspace and Sequenzy URLs remain as fallbacks.

## Company

* `create_api_key` - Create a company API key and return its one-time secret on the standard MCP surface. It is omitted from the OpenAI-reviewed surface; use `request_api_key_handoff` there
* `request_api_key_handoff` - Prepare an owner-confirmed link that opens the dashboard create-key form prefilled with a suggested name and permissions. Use it when key management is blocked because the active key lacks `api_keys:manage`. It creates nothing and never returns a key: hand the URL to the workspace owner and stop. Pass `replaceApiKeyId` (or `"current"`) to rotate, and the dashboard offers to revoke the predecessor once the replacement exists. Needs only `account:read`
* `list_api_keys` - List company API keys as non-secret metadata, including IDs, prefixes, permission receipts, usage timestamps, and the active-key marker
* `update_api_key` - Rename a company API key or replace its permissions in place, without issuing a new key. Reach for this when a call fails with a missing-scope error: the key value is unchanged, so no client has to be re-wired, and added permissions apply on the next retry. Removed permissions may remain usable for up to five minutes while API caches expire. `preset` and `scopes` replace the whole selection rather than merging into it, so call `list_api_keys` first
* `revoke_api_key` - Permanently revoke an exact company API key by ID after checking it with `list_api_keys` (`delete_api_key` is a compatibility alias)
* `get_company` - Get company details, product info, brand colors, AI writing context, effective localization settings, the read-only `emailBranding` entitlement, and `defaultSubscriberListIds` - the workspace default lists new contacts join when nothing targets them explicitly. `emailBranding.visible` reports whether "Sent with Sequenzy" is added to future sends; the reason, tier, subscription status, required action, and `subscriptionUrl` let an agent explain the entitlement and hand a billing-authorized user to the real **Account -> Subscription** page. Branding is injected at render/send time, so a paid entitlement removes it from future sends by existing live sequences without editing their blocks. It is not a writable `update_company` setting. A JSON `null` for `defaultSubscriberListIds` is every current and future list, not an empty selection; `[]` is no list at all. An ordinary profile or attribute upsert for an active contact keeps that contact's memberships unless it names lists explicitly; capture submissions and explicit lifecycle reactivations can reapply target or default lists. Change the setting with `update_company`. Read it before connecting an integration that has no per-integration list targeting of its own (Dodo Payments, PostHog, Polar, Paddle, and similar), because live contacts and payment-provider backfills land there. PostHog history imports are different: contacts created by that import have no list memberships. Send Time Optimization is not a company setting; audit campaigns with `list_campaigns` or `get_campaign`
* `get_app_urls` - Generate dashboard links for resources and company administration. When a company is resolved, the result includes the canonical `subscription` URL. The `settingsTab` values `billing` and `subscription` are compatibility aliases for **Account -> Subscription**; they do not invent a nonexistent Settings tab
* `update_company` - Edit product info, brand context, the brand `emailDesignPrompt` (art direction for AI-designed emails - layout, density, imagery, CTA prominence, separate from `toneVoice`; when empty, the next email generation prefills it with the direction derived from the brand, and an empty string clears it so the next generation writes a fresh one), and the default email theme (`emailTheme` accepts partial updates; `null` resets it - to restyle a single sequence email instead, patch that step's `emailTheme` with `update_sequence_node`), or set account-wide `fromEmail` and `replyTo` defaults (From domains must be verified); send `fromName` or `replyToName` alone to rename the existing default profile's display name, or pair them with `senderProfileId` / `replyProfileId` from `list_sender_profiles` to pick and rename a specific profile. Set `defaultSubscriberListIds` to choose which lists new contacts join when nothing targets them explicitly - forms, API writes, events, tag actions, imports, and every integration without its own targeting. The three states differ: `null` is every current and future list, `[]` is no list at all, and an array is exactly those lists. Unknown or foreign list IDs are rejected rather than skipped, and the change applies only to later writes - nobody is moved or removed retroactively, so a list already filled by a backfill still needs `remove_subscribers_from_list`. Profile, branding, and AI-context fields need `company_profile:manage` (included in Safer agent access); the sending-identity, reply-tracking, and `defaultSubscriberListIds` fields need `companies:manage`
* `add_sending_domain` - Add a sending domain and return the SPF, DKIM, MAIL FROM, inbound and optional company tracking CNAME records for setup. Optional `mailFromPrefix` chooses the bounce subdomain (`add_website` remains as a compatibility alias)
* `get_tracking_domain` - Read the company tracking domain, its status, whether links use it now, and its CNAME record
* `set_tracking_domain` - Set or change the company tracking domain every sending domain uses for tracked links and opens. Any subdomain works; it never blocks sending, and changing it keeps links in emails already sent working while the old CNAME stays
* `verify_tracking_domain` - Check the company tracking domain's CNAME and HTTPS certificate now
* `remove_tracking_domain` - Remove the company tracking domain; links in emails already sent through it stop working
* `configure_sending_domain` - Deprecated: set `<trackingPrefix>.<domain>` as the company tracking domain when none is set; prefer `set_tracking_domain`
* `list_websites` / `check_website` - Read stored aggregate, SPF, DKIM, MAIL FROM status, the envelope sender domain, and the Apple Email Source domains derived from sender profiles
* `verify_sending_domain` - Run a fresh DNS verification and return `verified` separately from `readyToSend` and selected home-transport readiness
* `get_email_design_system` - Get the visual identity every AI-generated email renders inside: the design code (kicker style, title alignment, button shape, divider style, density, sanctioned opener treatments) and the composition spine (`hero-led` | `editorial` | `product-spec`). The identity is stored as the company's design direction text; the tool returns the parsed tokens, the raw text, and `isDefault` while the identity is still purely derived from brand context
* `update_email_design_system` - Adjust that identity (partial update) or `reset` it to the brand-derived defaults; the change is written into the design direction text (custom prose is preserved) and applies to every future AI email generation and sequence enrichment. Requires `company_profile:manage`
* `get_sync_rules` - Get the automatic tag changes applied when events fire, plus whether the optional platform preset is active
* `update_sync_rules` - Replace the full sync rule set (`[]` disables rules; `null` opts into the inherited SaaS/ecommerce preset); rules support subscriber-tag conditions and a product match (tags, collections, product types, vendors) for commerce events
* `get_shopify_automation_settings` - Get the connected Shopify store's browse-abandonment, cart-abandonment, and price-drop settings with defaults applied
* `update_shopify_automation_settings` - Update browse-abandonment, cart-abandonment, and/or price-drop settings (partial update; `null` resets a section to the defaults)
* `list_integrations` - List connected integrations (Stripe, Shopify, Supabase, Clerk, ad platforms, and so on) with connection state, sync health, last sync time, and last sync error. `lastSyncSkipped` reports records the last store sync could not import normally, split into profiles that were kept but cannot receive email and records that were not imported at all. Credentials, access tokens, and webhook secrets are never returned; the OpenAI-reviewed surface also removes raw sync-error detail
* `get_integration` - Inspect one connected integration in depth: what it syncs, every event it emits and when, the tags each event applies through the account's sync rules, which sequences trigger on each event, 24h webhook activity, and prioritized recommendations. `unusedEvents` lists the events nothing acts on yet, `availableActions` says what can be called on it right now, `ingestion` reports the bulk sync state and which lists new contacts land on (with names), Shopify `pixel` carries the live storefront tracking state, and Webflow `webflowCapture` reports the exact selected native forms, retained fields, required double opt-in, list IDs, sequence toggle, webhook state, and schema fetch time. An empty `webflowCapture.forms` means capture is off. For Attio, `details.mappedListCount` 0 plus an `attio_lists_unmapped` recommendation means list joins are not synced until you map lists with `update_attio_settings`
* `list_integration_capabilities` - Describe what each provider does whether or not it is connected: category, connect method, what it syncs, every event with the moment that triggers it, attributes written, supported actions, availability, and caveats. Coverage differs sharply - only Shopify has storefront browse tracking, only Stripe and Chargebee cover the full trial lifecycle - so check here instead of assuming an event exists
* `get_event_schema` - Read what a built-in event payload actually contains: a real example payload per provider, plus every property path with its type, the merge tag that resolves it, and a note wherever the sample alone is ambiguous. Omit `eventName` to list every documented event. `documented: false` means no sample is published, never that the event name is invalid - custom events carry exactly the properties you send
* `connect_integration` - Connect an API-key or webhook-secret provider on the standard MCP surface (Polar, Paddle, Dodo, Lemon Squeezy, Whop, Creem, Chargebee, Clerk, PostHog, Segment, Affonso, or Attio). Credentials are validated where possible, stored encrypted, and never returned; payment providers queue their initial revenue backfill, and PostHog and Segment can import event history. The response includes the webhook URL to configure at the provider with the same secret. It is omitted from the OpenAI-reviewed surface; complete setup in the dashboard or local CLI instead
* `list_integration_activity` - Recent integration webhook and sync activity with action, status, event type, matched contact, and error. The log to read when an integration says connected but nothing is arriving. Retained 24 hours; payloads are sanitized so no credentials appear
* `disconnect_integration` - Disconnect Lemon Squeezy and remove its managed webhook. Inspect `cleanupWarning` and repeat the call to retry cleanup; local ingestion is already stopped. Other providers disconnect from the dashboard
* `set_integration_sync_enabled` - Turn bulk imports and backfills on or off. Disabling keeps the connection, credentials, and live webhook delivery active. Does not disconnect the integration, and does not stop it creating contacts
* `set_integration_list_targeting` - Choose which lists the contacts an integration creates join. This is the tool for "stop this integration adding people to my marketing lists": `set_integration_sync_enabled` only pauses bulk backfills and leaves the live webhook writing. Affects list membership only on future provider writes - `null` falls back to the workspace default lists, `[]` joins none, and nobody is ever removed from a list. Webflow deliberately rejects `null`; use `[]` or explicit list IDs, and configure its native forms, field allowlists, required double opt-in, and sequence enrollment in the dashboard. With no selected Webflow forms, capture is off and no contacts are created. Wix/Webflow submissions, Shopify customer updates, and Supabase resubscriptions can add existing contacts to new targets; Stripe targeting applies only when its webhook creates a subscriber. It does not stop contacts being created, attributes syncing, sync-rule tags, or default `any_contact` enrollments, which fire precisely because the contact joined no list. Explicit `any_list` and specific-list sequences require a matching membership and do not enroll a list-less contact; pair this tool with `pause_sequence_enrollments` when `any_contact` enrollment must stop too. Honored by Supabase, Stripe, Shopify, Wix, and Webflow. For a provider that is not on that list (Dodo Payments, PostHog, Polar, Paddle, WooCommerce, and similar), this tool returns an error and live contacts instead follow the workspace defaults - read and change those with `defaultSubscriberListIds` on `get_company` / `update_company`. A JSON `null` on that field means every list, not none; `[]` means none, remembering that the default is workspace-wide and repoints every other untargeted contact source too. PostHog history imports are an exception and create contacts without list memberships. Needs `integrations:manage`, which Safer agent access deliberately withholds
* `sync_integration` - Queue a manual re-sync: customers and revenue for a payment provider (Stripe, Polar, Paddle, Dodo, Lemon Squeezy, Creem, Chargebee, Whop), the user backfill for Supabase, or the event-history import for PostHog and Segment. The Supabase run reads the table already configured for that integration and cannot be pointed elsewhere; run it before a campaign that needs names or plan attributes on users who existed before the trigger was installed. The PostHog and Segment runs are the supported way to retry an event-history import that failed or was interrupted: they restart from the beginning with the stored credentials, and already-imported events dedupe. Returns immediately; poll `get_integration` for `syncStatus`. Other providers re-sync from the dashboard
* `get_integration_pixel` - Check whether a Shopify store's storefront tracking pixel is installed and reporting to this account. Read live from Shopify on every call, because a merchant can remove the pixel without Sequenzy hearing about it. `pixel.healthy` is the field to branch on, `pixel.error` distinguishes a confirmed outage from an unknown Shopify read, and `dependentEvents` names the events that rely on the pixel. Shopify only
* `activate_integration_pixel` - Install the storefront pixel, or repoint an existing one at this account. Idempotent (`changed: false` when already live). Events start arriving on the next storefront visit and nothing is backfilled, so run it before building the sequence. Fails with a message naming the reconnect step when the store granted an older permission set
* `get_attio_mapping` - Inspect a connected Attio integration's list mapping: Sequenzy lists, live Attio people-lists (id, api slug, name), the saved `listMap`, and `mappedListCount`. Call this when Attio is connected but mappings are empty, before `update_attio_settings`, so you have Attio list ids instead of asking the user to paste UUIDs. Reads Attio live. Attio only
* `update_attio_settings` - Set which Sequenzy lists sync into which Attio people-lists using the stored access token. Does **not** require the secret again — do not reconnect just to change mappings. `listMap` is a full replacement (`{}` clears every mapping). Unmapped lists are not synced; unmapping does not remove anyone from Attio. Needs `integrations:manage`, which Safer agent access deliberately withholds
* `list_web_tracking_keys` - List the publishable keys that let a non-Shopify website send on-site events into this workspace. Each returns its origin allowlist, `unrestricted`, `lastUsedAt`, and a paste-ready `installSnippet`. A key with `lastUsedAt: null` has not successfully authenticated an event yet; check deployment, instrumentation or traffic, and the origin allowlist
* `get_web_tracking_key` - One key with its install snippet and ingest endpoint. Use it to hand over the exact script tag rather than rebuilding one, since the snippet embeds both the key and the workspace id
* `create_web_tracking_key` - Create a publishable key and return the install snippet. This is what turns on product views, cart activity, and browse abandonment for a site that is not Shopify or WooCommerce. Always pass `allowedOrigins`; an empty allowlist accepts events from any site. Nothing is backfilled for the period before the snippet is deployed
* `update_web_tracking_key` - Rename a key, replace its origins (the list is replaced, not appended), or revoke it with `isActive: false`. Revoking preserves the key value so the matching snippet can still be found on the site
* `delete_web_tracking_key` - Permanently delete a key. Any page still running the snippet starts being rejected, so the snippet must come off the site too
* `list_sender_profiles` - List sender (From) and reply-to profiles, which are the account defaults, and whether each sender address sits on a verified sending domain
* `update_sender_profile` - Rename one sender or reply-to profile in place without changing which profile is the account default. Use it to standardize a display name across the several identities a mailbox can carry (for example `Viraj from SnapCount` down to `SnapCount`); pass `type: "reply"` for a reply-to profile. Only the name changes - the address, its sending domain, and the default From/Reply-To selection are untouched. To change which profile is the default, use `update_company` instead. Requires `companies:manage`
* `delete_sender_profile` - Permanently delete one sender (From) profile. It refuses to delete the last sender or a profile used by a live campaign, active sequence (including a step override), or transactional email. Eligible draft and rejected campaigns plus the account default move to the best remaining sender when needed; review `fallbackSenderProfileId` before sending. Reply-to profiles are not supported. Requires `companies:manage`
* `get_notification_preferences` - Get which account notifications Sequenzy emails the API key's own user for this company (new subscriber, form submitted, campaign finished, weekly report), with the modes each event supports and the platform defaults
* `update_notification_preferences` - Change those settings. Modes are `off`, `instant`, `daily`, and `weekly`; `daily` is only valid for `new_subscriber` and `weekly` only for `weekly_report`. Instant form-submission notifications stop after 50 per workspace per UTC day. The Monday weekly report is on by default for the workspace owner (off for invited members until they opt in) and only sent for weeks with more than 10 emails sent; set `weekly_report` to `off` to stop it. Useful before a large migration, though imports never trigger new-subscriber notifications and busy days fall back to a daily summary automatically. Never reads or writes a teammate's settings
* `get_tracking_settings` - Get open/click/unsubscribe tracking flags, the opt-in `strictBotFilteringEnabled` bot-detection flag, the default attribution window, automatic UTM tagging, the company tracking domain and its status, inbound reply-tracking settings, and the `consent` block with `doubleOptInEnabled`, the confirmation email ID, and the post-confirmation redirect URL
* `update_tracking_settings` - Turn open, click, and unsubscribe tracking on or off account-wide, opt in or out of `strictBotFilteringEnabled`, set the default attribution window, require double opt-in for new contacts with `doubleOptInEnabled`, set where the hosted confirmation page sends subscribers after they confirm with `doubleOptInRedirectUrl` (`null` keeps them on the branded confirmation page), and configure automatic UTM tagging (`autoUtmSettings` merges over the stored templates; a `null` field stops that parameter, and `null` for the whole object resets them to the defaults). Applies to emails sent afterwards; reply tracking stays on `update_company`. Enabling `doubleOptInEnabled` requires a sender profile and provisions the confirmation email automatically; it does not change contacts that are already active

Set `unsubscribeTrackingEnabled: false` with `update_tracking_settings` to send
unsubscribe links directly to `https://sequenzy.com`, even with a custom tracking
domain. This applies to subsequent sends. Actual unsubscribes
and their email attribution are still recorded.

* `get_sending_status` - Check whether company-level sending is `active`, `paused`, or `suspended`, why, and what it takes to restore it. Returns the pause reason and reason kind, the automated sender-health review state, whether one-click resume is available (and if not, which gate is blocking it), the enforcement counts and thresholds for permanent bounces, temporary bounces, and complaints, and ordered remediation steps. Requires only `account:read`
* `resume_sending` - Restore sending paused by a high permanent-bounce rate, after fixing the cause. Requires `listSanitizationConfirmed: true`, the `companies:manage` scope, and owner or admin access. It is a client of the same control the dashboard uses, not a bypass: only a permanent-bounce pause qualifies, the automated review must have cleared it, and it never removes suppressions

Call `get_sending_status` first 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, so a test send that "works" in configuration terms still never leaves - `send_test_email` and `send_sequence_test_email` return a `SENDING_PAUSED` error carrying the same reason and steps.

Do not tell the user to wait for a bounce rate to expire. Sender-health enforcement uses all-time totals counted from a reset watermark rather than a rolling window, so `metricsWindow.expiresAt` is always `null` and the rate does not decay. It only moves as more real (non-test) sends accumulate, or when a resume moves the watermark. Test sends are excluded from the denominator, so they cannot dilute it either.

Only pass `listSanitizationConfirmed: true` after the user has confirmed the remediation. It is recorded on the account's audit trail as their statement that the source of the invalid addresses is fixed and permanent bounces remain suppressed. When `selfResume.canSelfResume` is `false`, relay `selfResume.unavailableReason` and the `remediation.steps` instead of retrying - `waiting_for_review` clears on its own, while `blocked_by_ai`, `review_failed`, `blocked_by_admin`, and `unsupported_reason` need a support review.

For a new sending domain, call `add_sending_domain`, publish the records in `website.dnsRecords`, wait for DNS propagation, and then call `verify_sending_domain`. Treat `website.readyToSend` as the sending gate: DNS may be verified while the selected SES home region is still activating. Checking or verifying an unconfigured domain returns a recovery message that points back to `add_sending_domain`.

For a site that is NOT Shopify - a custom storefront, headless shop, marketplace, or SaaS marketing site - the equivalent check is `list_web_tracking_keys`. The same events (product views, collection views, search, cart activity, and the browse and cart abandonment built on them) require a publishable key and an installed script tag, and a workspace with no key, a revoked key, or a key that has never been used produces none of them. Create one with `create_web_tracking_key` and relay `installSnippet` verbatim. Identified events also require a short-lived token minted by the customer's authenticated backend through `POST /api/v1/web-tracking-identities`; tell them to call `sequenzy.identify(email, identityToken)` at sign-in and checkout. A publishable key alone only records anonymous activity and cannot trigger subscriber automation.

For Shopify specifically, check `get_integration_pixel` (or the `pixel` field on `get_integration`) before building anything that depends on on-site behavior. Browse abandonment, cart recovery, product views, collection views, and storefront search all come from the storefront pixel, and a store with a confirmed missing or stale pixel produces none of them - the sequence is built correctly, enrolls nobody, and no error appears anywhere. If `pixel.error` is set, retry rather than treating the unknown state as an outage. `activate_integration_pixel` repairs a confirmed unhealthy configuration without a dashboard visit.

Call `get_event_schema` before writing any `{{event.*}}` merge tag or event property filter. `list_integration_capabilities` names the events a provider emits; this shows what is inside one. Guessing is not safe here: an unrecognized merge tag renders as an empty string rather than an error, so a wrong property name ships silently broken. It is also the only place that states what a sample cannot - `*Cents` fields are minor units while `predictedLtv` is whole currency units, `price` is a preformatted display string, `churnRisk` is a 0-95 percent, and `orderId` is a string on Shopify but a number on WooCommerce. Custom event names are absent by design and are still fully supported.

Before building automations on an integration, call `get_integration`. It answers whether the work is "connect something" or "turn on a sequence": `unusedEvents` names events with no listening sequence, and `recommendations` flags listeners that are not accepting enrollments. `observedByAccount`, `accountLastSeenAt`, and `accountNeverReceivedEvents` are explicitly account-wide because another integration or the public events API can update the same event definition. Use the retained activity log for connection-specific delivery diagnosis. On the standard MCP surface, `connect_integration` accepts credentials the user explicitly provides; prefer the dashboard or local CLI when secrets should stay outside the AI conversation. The OpenAI-reviewed surface omits this tool. Use `disconnect_integration` for Lemon Squeezy; other provider disconnects remain in the dashboard.

## Sequences

* `list_sequences` - List sequences with dashboard-compatible status, search, label, limit, and offset filters. Branch on `effectiveStatus` (`draft`, `live`, `enrollment_paused`, `paused`, `archived`) rather than `status`, which reads `active` even when new enrollments are paused; `acceptsNewEnrollments`, `processesExistingEnrollments`, and the plain-language `effectiveStatusSummary` are returned alongside it. The legacy `triggerConfig.active` flag is not read by the runtime and now mirrors `acceptsNewEnrollments`
* `get_sequence` - Get sequence details plus normalized `sequence.nodes` with each node's `id`, `nodeType`, current `config`, `updatedAt`, and `updateHints` describing editable/managed fields and the concurrency token to return; reusable `sequence.edges`, `graphRevision`, and editable `sequence.emails` are also returned, including each linked email's effective `emailPreset` (`branded` or `minimal`). `sequence.emails` covers every email-sending step, including `action_ab_test` steps - those report `nodeType: "action_ab_test"` with an `abTest` summary whose `variants[]` entries include full `blocks` when `ab_tests:read` is granted, and their step-level copy is control variant A only (see [editing the content of a sequence A/B step](#editing-the-content-of-a-sequence-ab-step)). Send Time Optimization is campaign-only; sequences use `sendingWindow` (allowed local hours/days), which is a shared gate, not per-subscriber predicted send times
* `simulate_sequence` - Dry-run a sequence without sending mail or enrolling anyone. Nobody is auto-enrolled on activate. Without a subscriber it reports who currently matches and activation readiness errors. Pass `subscriberId` or `email` to walk that stored contact's branch path, and `limit` (1-25, default 10) to control the current-match sample. Call this before `enable_sequence`
* `send_sequence_test_email` - Send one saved `action_email` step to 1-10 internal reviewers without enabling the sequence or enrolling subscribers. Pass the `sequenceId` and `nodeId` from a `get_sequence` email entry only when its `nodeType` is `action_email`; `action_ab_test` variants are not supported by this tool and should be inspected on `get_sequence.sequence.emails[].abTest.variants`. Each result includes a durable `emailSendId` for `get_email_send`
* `create_sequence` - Create a sequence with:
  * Only `name` for a blank, disabled trigger-to-completion draft matching the dashboard; `trigger` defaults to `contact_added`
  * Dashboard metadata (`description`, `labels`, `userCancellable`, and sequence BCC recipients) plus full From/Reply-To identity
  * `trigger: "inbound_webhook"` with integration metadata, in addition to list, tag, segment, event, inactivity, and frequency triggers
  * `trigger: "segment_entered"` plus `segmentId` for saved-segment entry automations
  * `trigger: "segment_exited"` plus `segmentId` to enroll active contacts who stop matching a saved segment
  * `listIds` for a `contact_added` trigger covering several lists, and `tagNames` for a `tag_added` trigger covering several tags - joining or receiving ANY of them enrolls the contact (up to 25 values each). Use these when the user says "when added to list A or list B"
  * `listScope` for `contact_added` triggers with no list at all: `any_contact` (the default) enrolls every contact that is added, including the list-less contacts an integration creates when its list targeting is empty, while `any_list` waits until the contact joins a list. It cannot be combined with `listId`/`listIds`
  * `goal` - AI generates email content
  * `durationDays` - total duration used to space AI-generated emails when using `goal`
  * `emailStyle` - `visual` (designed) or `plain` (text-first) for AI-generated emails; defaults to the company's saved preference
  * `steps` with `blocks` - Sequenzy JSON block format
  * `steps` with `html` - Any HTML (React Email, MJML, provider exports, etc.) preserved as one raw HTML block
  * `steps` with `attachments` - URL-backed file attachments (`[{ filename, path }]`) fetched at send time. For event-triggered sequences, `path` can use an event value such as `{{event.file_url}}`, and `filename` can also contain merge tags (max 10 per email, 15MB total)
  * `enrollmentMode: "matching_field"` for event-triggered product-, variant-, order-, or subscription-specific sequences that should block duplicate active runs only for the same resolved field
  * A scalar `enrollmentFieldPath`, such as `order.id` or `product.providerVariantId`, when you want `matching_field` to use a custom event property. Array traversal with `[]` is supported by `propertyFilters`, not enrollment keys
  * `propertyFilters` for `event_received` triggers - only start the sequence when the event properties match, e.g. scope a purchase sequence to one product with `{ "path": "lineItems[].providerProductId", "operator": "equals", "value": "prod_123" }` (or `productIds` for Stripe `saas.purchase` events)
  * For `event_received` sequences, step content can use `{{event.amount}}`, `{{event.order.id}}`, or other `{{event.*}}` merge tags from the event payload that enrolled the subscriber
  * Custom-event responses include `eventTrackingCode` plus `eventTracking`, which gives the endpoint, normalized trigger filters, a generated payload example, `examplePayloadMatchesFilters`, the direct docs URL, and arguments for `get_integration_guide`. When the example does not satisfy every filter automatically, follow `examplePayloadNote` and adapt it before sending
* `update_sequence` - Modify a sequence, target a specific step with the `emailId` or `nodeId` returned by `get_sequence` (email steps only - `action_ab_test` steps keep their copy on the test's variants), atomically replace its typed trigger, or update `enrollmentMode` / `enrollmentFieldPath`. Set `bccEmails` to blind-copy team inboxes on every email the sequence sends (use `clearBccEmails` to remove them). Email steps also accept `attachments` (`[{ filename, path }]`) - URL-backed files fetched and attached at send time; `path` may be an `{{event.*}}` URL template resolved separately for each enrollment, and `[]` removes them. Tag, list, wait, condition, and webhook steps added through `insertSteps` or a `branch` path carry 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, `action_webhook` takes an HTTPS `url` with optional `method` (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`), `headers`, a JSON `body` template, `resultKey` (saves the response for later `{{webhooks.KEY.data.field}}` merge tags), and `onError` (`continue`/`exit`/`fail`), and `action_ai` takes a merge-tag `prompt`, a required `resultKey`, `outputFields` with per-field fallbacks whose combined limits fit a 2000-token response budget, and optional `includeTags`/`includeEventProperties`/`includeRecentEvents` (with `recentEventLimit`, 1-50, default 10)/`includeAttributes` context selectors so later steps can read `{{ai.KEY.field}}`; SMS, discount, and delay steps keep using their step-level fields
* `update_sequence_node` - Patch one existing node in place using its `nodeId`. It cannot edit the copy of an `action_ab_test` step, which lives on the test's variants - read those blocks from `get_sequence.sequence.emails[].abTest.variants` and change each one with `update_ab_test_variant`. It supports every stored node type: delays, email/SMS content, actions, conditions, branch configuration without topology changes, webhooks, and triggers. Pass the node's latest `updatedAt` as `expectedUpdatedAt` to prevent stale writes. `action_email` nodes accept `emailTheme` to restyle that one step's linked email - `{ "emailTheme": { "colors": { "background": "#ffffff" } } }` repaints only that email's background and leaves the account-wide theme and every other email alone. It is a partial patch merged into the email's current theme (or the company theme when the step has no override yet); `null` drops the override so the step follows the company theme again, and `get_sequence` echoes the stored override back as `emailTheme`
* `update_sequence_nodes` - Apply multiple type-aware node patches atomically. Use this for bulk edits such as changing every 5-minute delay to 7 days, or restyling several email nodes at once with `{ "emailPreset": "minimal" }` or `{ "emailTheme": { "colors": { "background": "#ffffff" } } }`; either every patch commits or none do
* `edit_sequence_graph` - Atomically move, reconnect, delete, or deep-copy existing sequence nodes using the latest `graphRevision`; deleting a step immediately moves parked recipients to its unique surviving successor, or completes them when no successor remains, and returns `migratedRecipientCount` / `completedRecipientCount`; active sequences require explicit structural-change confirmation
* `insert_sequence_step` - Insert a typed email, SMS, delay, discount, subscriber-update, tag/list, outbound webhook, AI, condition, `logic_wait_for_event`, or `logic_branch` step. AI steps (`type: "ai"`) accept a merge-tag `prompt`, a required `resultKey`, `outputFields` with per-field fallbacks, and optional context selectors; later steps read the output with `{{ai.KEY.field}}` merge tags. Webhooks accept an HTTPS URL, a GET/POST/PUT/PATCH/DELETE method, string-valued headers, a JSON `body` template, a `resultKey` that saves the response for later `{{webhooks.KEY.data.field}}` merge tags, and `onError` failure behavior; URL, headers, and body support merge tags resolved at execution time. Wait nodes accept `eventName`, `timeoutDays`, and `timeoutAction`. Branch paths accept typed conditions plus `targetNodeId`/`elseTargetNodeId`, new path steps, or both, so one atomic call can route a reply path to completion and Else to an existing follow-up. Pass `splitMode: "random"` with `randomPercentages` to make the branch a weighted A/B split instead, which routes by percentage rather than by condition and has no else path
* `enable_sequence` / `disable_sequence` - Control status
* `duplicate_sequence` - Create an independent draft copy, including graph, emails, and sequence A/B tests
* `create_sequence_from_example` - Clone a public email gallery sequence into a draft with the example's trigger and timing (up to 12 emails); AI writes each email in your brand in the background, so poll `get_sequence` until `enrichmentStatus` is `complete`
* `archive_sequence` / `unarchive_sequence` - Move a sequence into the dashboard archive or restore it as a draft
* `list_sequence_goals` / `create_sequence_goal` / `update_sequence_goal` / `delete_sequence_goal` - Manage the persisted event, subscriber-attribute, or tag-applied conversion goals shown by the dashboard
* `get_sequence_inbound_webhook` / `configure_sequence_inbound_webhook` / `rotate_sequence_inbound_webhook_secret` - Read and configure the secret endpoint, field mapping, sample payload, and setup state attached to an inbound-webhook sequence trigger on standard MCP. The OpenAI-reviewed surface removes credential-bearing URLs from read/configure results and omits secret rotation; use the returned sequence dashboard URL there
* `enroll_sequence_audience` - Estimate (default) or, with `dryRun: false`, start a background run that enrolls a whole audience (`{ type: "all" }`, lists, a segment, a filter, or rules) with no per-call cap. Contacts already in the sequence are skipped and unsubscribed or bounced contacts are never enrolled. This is how a manual-trigger countdown sequence gets its audience; late enrollees skip steps whose key date already passed. Poll with `get_sequence_audience_enrollment`, list runs with `list_sequence_audience_enrollments`, stop one with `cancel_sequence_audience_enrollment`.
* `enroll_subscribers_in_sequence` - Manually enroll up to 500 subscribers by email, subscriber ID, or both. Only active subscribers are enrolled: unknown emails are returned in `notFound`, and inactive, unavailable, or already enrolled subscribers are counted in `skipped`. Pass `targetNodeId` (a non-trigger `nodeId` from `get_sequence`) to start at a specific step instead of the first step after the trigger. The sequence must be accepting entrants.
* `list_sequence_enrollments` - List the individual contacts enrolled in a sequence, with the node each one is sitting on. This is the contact-level view behind `get_sequence_stats` `enrollmentCounts`: filter by `currentNodeId` (IDs come from `enrollmentCounts.byCurrentNode` or `get_sequence`), `status` (defaults to active and waiting), `subscriberId`, or `email`. Each row carries `email`, `firstName`, `lastName`, `currentNodeLabel`, `currentNodeType`, `enrollmentStartedAt`, `waitUntil` (when a waiting contact resumes), `failedReason` (why a `failed` enrollment stopped; `null` otherwise), and `movedFromNodeId`/`movedAt`/`moveReason` (set when `move_sequence_enrollments` released the contact onto its current step), `enteredVia` (the list, tag, segment, event, inactivity check, or frequency check that enrolled the contact - the way to tell entrants apart when a trigger covers several lists or tags), `entryContext` (event id and property keys, never values), and `branchDecisions` (redacted if/else verdicts with the compared field name and a missing/empty/nonempty/equals\_expected summary). Pass `status: "failed"` to triage a step that is not delivering - failures are terminal, and the same `failedReason` on one `currentNodeId` across several contacts points at that step rather than at the contacts. Sort with `wait_until_asc` to see who moves next, and page with `limit` (up to 500) and `offset` until `pagination.hasMore` is `false` to export the full list. The response always echoes the sequence's single configured `stopCondition`, including any `matchConfig` event-property filters, field comparison, or `entry_audience` resolver; entry-audience defaults use `value: null` and resolve the enrolling tag or list 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 can keep reporting `waiting` until its delay expires. Pass `stopConditionMatch: true` to inspect the current state and read `stopConditionMatches` per row, where `null` means "not determined" rather than "does not match". This is a non-atomic snapshot: a step already past its stop check may still finish. See [when a stop condition actually cancels](/concepts/sequences#when-a-stop-condition-actually-cancels).
* `get_sequence_enrollment` - Read one enrollment token, including bounded ClickHouse `nodeHistory`. Use this when a completed token lists as `enteredVia` unknown or Sequence completed with no email: reconstructed branch decisions are redacted (field name + missing/empty/nonempty/equals\_expected), never the raw compared value. Check `nodeHistoryTruncated` and `branchDecisionsTruncated` before treating either audit trail as complete. Take `enrollmentId` from `list_sequence_enrollments`.
* `cancel_sequence_enrollments` - Stop active or waiting enrollments in a required sequence. Target `cancelAll: true` to drain every enrollment (the right move when segment-triggered contacts share no entry field value, and the only way to stop contacts already mid-flight - pausing enrollment just blocks new entrants), `subscriberIds` for a batch of up to 500, `subscriberId` for one contact, or entry-event `fieldValues`. Field-value cancellation can use `fieldPath` or the sequence's configured `enrollmentFieldPath`. Every bulk target defaults to `dryRun: true`; pass `dryRun: false` to apply, then repeat the call while the response reports `remainingCount` above zero.
* `move_sequence_enrollments` - Release a bounded batch of contacts off one sequence step and onto another, keeping their enrollment. Use this instead of cancel-then-re-enroll when you want the next N contacts waiting on a delay to continue early: cancelling discards the enrollment's entry event properties, stop-condition snapshots, and start date, and re-enrolling is refused while new enrollment is paused. Requires `fromNodeId`; `targetNodeId` defaults to that step's only next step. `limit` defaults to 100 and caps at 500, `sort` defaults to `wait_until_asc` (longest-waiting first), `dailyLimit` refuses to release more than that many onto the target step in a rolling 24 hours, and `tags` marks the released wave with existing tag names (needs `subscribers:tag`). Defaults to `dryRun: true`; pass `dryRun: false` to apply, then repeat while `remainingCount` is above zero. Moved contacts become active immediately, so the sequence emails them as soon as a worker picks them up.
* `realign_sequence_enrollments` - Pull waiting enrollments in a required sequence forward to the start of its sending window on the day they are already scheduled for. Run it after changing `sendingWindow` (or a wait-until-weekday step) on a live sequence: 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. Sequence windows never advance SMS, webhooks, branches, or other non-email actions. A wait only ever moves earlier, never onto a different local day, and never before now; nobody is cancelled or re-enrolled. Narrow it with `nodeIds` or `subscriberIds`. Defaults to `dryRun: true`; pass `dryRun: false` to queue an applied job.
* `get_sequence_enrollment_realignment` - Poll the `jobId` returned by an applied `realign_sequence_enrollments` call. When `status` is `completed`, inspect `result`; if `result.hasMore` is true, queue the next bounded apply with `result.nextCursor` as `cursor`.

Email-step `blocks` on `create_sequence`, `update_sequence`, and
`insert_sequence_step` are validated against the same block schema campaigns and
transactional emails use. A structurally invalid block is rejected with the step,
block index, and field named rather than stored, because a stored block the
renderer cannot render fails every enrollment at that step. Fields that parse but
will not render as their name suggests come back in an advisory `warnings` array
on success, so treat a quiet response - not just a successful one - as
confirmation that everything you sent took effect. A standalone `button` block may
be written with `buttonText` and `buttonUrl`, which are accepted as aliases for
`text` and `url`.

For node updates, call `get_sequence` immediately before writing. Start with the
returned node `config` and send only changed fields. Delay nodes use a readable
patch such as `{ "delay": { "days": 7 } }`, `delayMs`, `waitUntil`, or
`waitUntilWeekday` (for example `{ "waitUntilWeekday": { "day": "sunday",
"startTime": "09:00", "endTime": "12:00", "timezone": "America/Los_Angeles" } }`
to hold the next step until the next Sunday-morning window; contacts already
inside the window continue immediately). A `waitUntilWeekday` delay placed
immediately before an email is a hard timing gate for that send - any
intervening step can move delivery outside the weekday window. A
sequence-level `sendingWindow` holds emails at send time but does not
reschedule the graph. Node
updates for `action_email` can set `{ "emailPreset": "minimal" }` to change
only that linked email's **Style > Format** without changing the company theme.
This applies the same transformation as the dashboard to native Sequenzy
blocks, including emails that contain 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`.
Existing node patches and arbitrary graph topology are intentionally separate:
use `insert_sequence_step` for a new typed branch or wait node, and use
`edit_sequence_graph` to add/remove branch paths, reconnect edges, reorder
nodes, or convert the flow structure. Updating an active sequence requires
`confirmLiveChange: true` after
the user confirms the impact. Recipients already waiting keep their existing
scheduled timestamp; the new delay applies to recipients that reach that node
after the update.

For the common “suppress the second email after a reply” flow, call
`get_sequence`, take the first email, follow-up, and completion node IDs, then
insert the branch in one request:

```json theme={null}
{
  "sequenceId": "seq_123",
  "type": "logic_branch",
  "afterNodeId": "node_email_1",
  "branches": [
    {
      "id": "replied",
      "conditionType": "event_received",
      "eventName": "email.replied",
      "activityScope": "this_sequence",
      "targetNodeId": "node_sequence_complete"
    }
  ],
  "elseTargetNodeId": "node_email_2"
}
```

To A/B test two offers against each other inside a live sequence, insert the
same node with `splitMode: "random"`. Each path becomes a weighted variant, no
subscriber attribute is evaluated, and the percentages must sum to 100. A
random split has no else path, so omit `conditionType`, all condition-specific
fields, `elseSteps`, and `elseTargetNodeId`:

```json theme={null}
{
  "sequenceId": "seq_123",
  "type": "logic_branch",
  "afterNodeId": "node_cart_wait",
  "splitMode": "random",
  "randomPercentages": [50, 50],
  "branches": [
    {
      "id": "discount",
      "label": "10% off",
      "steps": [
        { "subject": "Here is 10% off", "html": "<p>Complete your order.</p>" }
      ]
    },
    {
      "id": "bonus",
      "label": "Free shipping",
      "steps": [
        {
          "subject": "Free shipping on us",
          "html": "<p>Complete your order.</p>"
        }
      ]
    }
  ]
}
```

Use a random split when the variants differ by more than the email itself, such
as a different delay or a different number of follow-ups on each path. To test
two versions of one existing email step and have a winner selected
automatically, call `create_ab_test` with `automationNodeId` instead.

### Branches inside a branch path

A path's `steps` array builds one linear chain of nodes, so it cannot contain
another branch. Nested branches are still supported - insert them as their own
`insert_sequence_step` call:

1. Insert the outer branch. End each path with the step the nested branch
   should follow, such as the wait before the flow re-evaluates the contact.
   Every path's last step automatically connects to whatever already followed
   `afterNodeId`, so the shared steps after the branch exist only once.
2. Read `addedBranchPathNodeIds` from the response. It maps each branch ID to
   that path's new node IDs, in order, so no extra `get_sequence` call is
   needed.
3. For each path that needs to re-evaluate, insert a second `logic_branch` with
   `afterNodeId` set to that path's last node ID. Its own paths reconnect to the
   same shared step downstream.

```json theme={null}
{
  "sequenceId": "seq_123",
  "type": "logic_branch",
  "afterNodeId": "node_day_14_wait",
  "branches": [
    {
      "id": "catalogue_empty",
      "conditionType": "does_not_have_tag",
      "tagName": "has-catalogue",
      "steps": [
        { "subject": "Upload your first track", "html": "<p>Get started.</p>" },
        { "type": "delay", "delay": { "days": 7 } }
      ]
    }
  ],
  "elseTargetNodeId": "node_final_email"
}
```

The response returns
`addedBranchPathNodeIds: { "catalogue_empty": ["node_a", "node_b"], "else": [] }`.
Insert the Day 21 re-check after `node_b`, and each of its paths ends at
`node_final_email` without copying that email:

```json theme={null}
{
  "sequenceId": "seq_123",
  "type": "logic_branch",
  "afterNodeId": "node_b",
  "branches": [
    {
      "id": "still_empty",
      "conditionType": "does_not_have_tag",
      "tagName": "has-catalogue",
      "steps": [
        { "subject": "Still nothing uploaded?", "html": "<p>We can help.</p>" }
      ]
    }
  ],
  "elseSteps": [
    { "subject": "Nice catalogue", "html": "<p>Here is what is next.</p>" }
  ]
}
```

Passing a `logic_branch` object inside a path's `steps` array is rejected with a
message pointing at this pattern. For a check that only needs to gate the flow -
continue when it passes, exit the sequence when it does not - use a
`nodeType: "logic_condition"` step inside the path instead; it stays linear and
needs no second call.

## Transactional

* `list_transactional_emails` - Search and filter transactional templates by name, slug, subject/title, or active state; sort by delivery metrics; and return each template's dashboard URL
* `get_transactional_email` - Read a transactional email by ID or slug, including body blocks
* `create_transactional_email` - Create a transactional template from a prompt, raw HTML, or Sequenzy blocks. Prompt-generated templates include company branding with no unsubscribe link
* `update_transactional_email` - Update transactional email metadata or body content
* `delete_transactional_email` - Permanently delete a saved transactional template by ID or slug, freeing the slug for reuse. Past deliveries and their stats are kept, and the linked email content stays as a reusable template returned as `deleted.emailId`, which `delete_template` can remove separately. To stop sends without losing the template, set `enabled` to `false` with `update_transactional_email` instead
* `send_email` - Send one email using direct `subject` and `html` content (mapped to the transactional API's `subject` and `body` fields), or pass a saved transactional email API slug through the compatibility-named `templateId` field. `to`, `cc`, and `bcc` each accept a single address or an array of up to 50, so a transactional send delivers one email with a shared recipient list rather than one email per address. Addresses repeated across the fields are dropped from the lower-priority one (`to` beats `cc` beats `bcc`), and the result echoes the accepted lists. Marketing sends still take exactly one `to` address and reject `cc` and `bcc`, because suppression and one-click unsubscribe apply per subscriber. Its `variables` object supports nested arrays for repeat blocks, such as `{ "event": { "items": [...] } }`. Attachments accept Base64 `content` or a public `path` (up to 10 files / 15MB total); set `contentId` to embed a CID image referenced from the HTML and `contentType` to override MIME detection. When a single recipient matches a subscriber, saved first and last names fill omitted name variables; explicit `variables` values take precedence. The result echoes the accepted `emailType`. Optional `fromEmail`/`fromName` and `replyTo`/`replyToName` pick an existing verified brand identity for that one send without profile IDs; they do not create profiles. `fromEmail` looks up a send-ready sender by address, and if several identities share the address pass `fromName` (or `senderProfileId` from `list_sender_profiles`). `senderProfileId`/`replyProfileId` remain available when you already have them. When those fields are omitted, a template send keeps its saved From and Reply-To identities, while a direct send uses the company defaults. `replyTo` remains a one-off Reply-To string and is mutually exclusive with `replyProfileId`. `{{viewInBrowserUrl}}` becomes a hosted copy link. When `trackingSettings` is omitted, the account's Transactional API tracking defaults apply. Set `clickTracking` or `openTracking` to `false` to opt out for that send only; these fields cannot enable tracking disabled by the account defaults. The optional `headers` object adds extra email headers, such as your own `List-Unsubscribe` on a transactional send; marketing sends keep Sequenzy's signed unsubscribe headers, and headers Sequenzy manages are never applied. Header values must be strings. Headers the API does not apply are listed in the result's `ignoredHeaders` with a reason instead of failing the send.
* `get_transactional_stats` - Inspect top clicked links, complaints, replies, bounce classifications, and separate human/machine engagement for one saved template
* `list_email_sends` - Search recent delivery history and return a dashboard URL on every delivery; every row carries `subscriberId`, `automationNodeId`, and `abTestVariantId`, and a returned ID with `get_email_send` gives the full timeline

## Segments

* `list_segments` - List saved segments
* `create_segment` - Create a segment from explicit filters, including nested AND/OR groups, event filters, segment filters, Stripe product purchase filters, and optional `filterJoinOperator`
* `update_segment` - Update a segment's name and/or replace its filter rules using the same `filters` plus `filterJoinOperator` or nested `root` shapes as `create_segment`
* `delete_segment` - Permanently delete a segment; subscribers are not affected
* `get_segment_count` - Preview how many active subscribers match a segment

For Stripe product segments, use the product-specific Stripe fields. The supported shapes are:

* `{"id":"filter-1","field":"stripeProduct","operator":"is","value":"prod_pro"}` for "bought product"
* `{"id":"filter-1","field":"stripeProduct","operator":"is_not","value":"prod_pro"}` for "didn't buy product"
* `{"id":"filter-1","field":"stripeProduct","operator":"at_least","value":"prod_pro:3"}` for "at least 3 payments"
* `{"id":"filter-1","field":"stripeProduct","operator":"less_than_count","value":"prod_pro:3"}` for "fewer than 3 payments"
* `{"id":"filter-1","field":"stripeCurrentProduct","operator":"is","value":"prod_pro"}` for "currently has product"
* `{"id":"filter-1","field":"stripeTrialProduct","operator":"is","value":"prod_pro"}` for "currently trialing product"
* `{"id":"filter-1","field":"stripeTrialProduct","operator":"is","value":"prod_pro:is_canceled"}` for "trialing product is set to cancel"
* `{"id":"filter-1","field":"stripeTrialProduct","operator":"gte","value":"prod_pro:start_at:7 days ago"}` for "trial started in the last 7 days"
* `{"id":"filter-1","field":"stripeTrialProduct","operator":"is","value":"prod_pro:end_at:2026-05-26"}` for "trial ends on May 26, 2026"

For products purchased through commerce orders (Shopify, WooCommerce, or the Commerce API), use the `commerceProduct` field. The value is `provider:productId` - product IDs are provider-scoped, so the provider prefix (`shopify`, `woocommerce`, or `api`) tells the filter which catalog the ID belongs to. A bare product ID matches the ID on any provider:

* `{"id":"filter-1","field":"commerceProduct","operator":"is","value":"api:prod-starter-kit"}` for "bought product"
* `{"id":"filter-1","field":"commerceProduct","operator":"is_not","value":"api:prod-starter-kit"}` for "didn't buy product"
* `{"id":"filter-1","field":"commerceProduct","operator":"at_least","value":"shopify:42:2"}` for "placed at least 2 orders containing the product"
* `{"id":"filter-1","field":"commerceProduct","operator":"less_than_count","value":"shopify:42:2"}` for "fewer than 2 orders containing the product"

To segment on a whole collection instead of one product, use the `commerceCollection` field. The value is a collection ID or handle, optionally provider-prefixed, with the same optional order-count threshold:

* `{"id":"filter-1","field":"commerceCollection","operator":"is","value":"skincare"}` for "bought anything from the collection"
* `{"id":"filter-1","field":"commerceCollection","operator":"is_not","value":"skincare"}` for "never bought from the collection"
* `{"id":"filter-1","field":"commerceCollection","operator":"at_least","value":"shopify:skincare:2"}` for "placed at least 2 orders from the collection"

Collection membership is resolved from the synced catalog at evaluation time, so it reflects which products are in the collection now rather than at purchase time.

For engagement filters (`emailSent`, `emailDelivered`, `emailOpened`, `emailClicked`, `emailBounced`, `emailComplained`), the `value` can be a rolling time window (`7d`, `30d`, `90d`, `180d`, `all`), a specific sent campaign via `campaign:<campaign_id>`, an email-type scope via `marketing:<timeRange>` (marketing-policy campaign, automation, and Send API traffic) or `transactional:<timeRange>` (transactional-policy sends), or - with the `at_least` / `less_than_count` operators - a count with a time window in `count:timeRange` format:

* `{"id":"filter-1","field":"emailClicked","operator":"at_least","value":"10:all"}` for "clicked 10 or more times ever"
* `{"id":"filter-1","field":"emailOpened","operator":"less_than_count","value":"2:90d"}` for "opened fewer than 2 times in the last 90 days"
* `{"id":"filter-1","field":"emailSent","operator":"is_not","value":"marketing:7d"}` for "no marketing email in the last 7 days" (email-type values work with `is`, `is_not`, and the two bounce-subtype operators; not count operators)

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 to express rules like "bounced campaign A but not campaign B":

* `{"id":"filter-1","field":"emailBounced","operator":"is","value":"campaign:cmp_abc"}`
* `{"id":"filter-2","field":"emailBounced","operator":"is_not","value":"campaign:cmp_xyz"}`

Use `list_campaigns` to look up the campaign IDs.

By default, segment filters use `AND` logic. To match any filter instead of all of them, pass `filterJoinOperator: "or"` when calling `create_segment`.

For nested logic, pass a v2 `root` group instead of `filters`:

```json theme={null}
{
  "name": "Active non-buyers",
  "root": {
    "kind": "group",
    "id": "root",
    "joinOperator": "and",
    "children": [
      {
        "kind": "filter",
        "id": "filter-1",
        "field": "attribute",
        "operator": "lte",
        "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"
          }
        ]
      }
    ]
  }
}
```

Event filter values use `eventName:range` for `is` / `is_not` and `eventName:count:range` for `at_least` / `less_than_count`. Segment filter values are saved segment IDs.

`list_segments` returns both:

* `subscriberCount` - all matched contacts, including unsubscribed or bounced contacts
* `activeSubscriberCount` - contacts eligible for campaigns

`get_segment_count` returns the active count, because campaigns and most send flows only target active 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).

* `list_audience_syncs` - List segment-to-audience syncs with schedule and last sync status
* `list_ad_accounts` - List the Meta ad accounts available for syncing
* `create_audience_sync` - Create a sync from an existing segment (`segmentId`) or a ready-made template (`predefinedSegmentId`, for example `zero-ltv`, `no-purchase-1y`, `recent-buyers`); the first upload runs immediately
* `update_audience_sync` - Change the frequency (`hourly`, `daily`, `weekly`) or pause/resume via `isActive`
* `delete_audience_sync` - Remove a sync; the Meta audience itself is kept
* `sync_audience_now` - Trigger an immediate upload outside the schedule

Audiences are add-only: subscribers who leave the segment stay in the Meta audience. Meta requires 100+ matched people before an audience can be used for ad delivery.

## Warehouse Sync

Import contacts and events from the user's Snowflake, BigQuery, Redshift or Postgres. See [Warehouse Sync](../integrations/warehouse-sync).

* `list_warehouse_connections` - List connections with status and number of syncs
* `create_warehouse_connection` - Connect a warehouse with a read-only user; a test query runs before saving
* `update_warehouse_connection` - Rename, change settings or rotate credentials (re-tested)
* `delete_warehouse_connection` - Delete a connection that has no syncs
* `test_warehouse_connection` - Re-run the test query
* `preview_warehouse_query` - Run a query with a 20-row limit to see its columns
* `list_warehouse_syncs` / `get_warehouse_sync` - Syncs with status, cursor position and the latest run
* `create_warehouse_sync` - Sync contacts (`subscribers`) or events on existing contacts (`events`) from a query on a schedule
* `update_warehouse_sync` - Change the query, mapping, cursor, schedule or options, or pause/resume via `isEnabled`
* `delete_warehouse_sync` - Stop a sync; imported data stays
* `run_warehouse_sync` - Run now, optionally with `fullResync`
* `list_warehouse_sync_runs` - Row counts, sample problems and import progress per run

`create_warehouse_connection`, `update_warehouse_connection` and `preview_warehouse_query` are not available on the OpenAI-reviewed MCP surface.

## Data Exports

Stream workspace data to the user's own S3, Google Cloud Storage or S3-compatible bucket. See [Data Exports](../integrations/data-exports).

* `list_data_exports` - List exports with status, per-dataset progress (`exportedThrough`) and the last error
* `get_data_export` - Get one export
* `create_data_export` - Add a bucket; a test file is written first and the first export starts immediately
* `update_data_export` - Rename, change datasets or frequency, pause/resume via `isEnabled`, move, or rotate credentials (`accessKeyId` with `secretAccessKey`)
* `delete_data_export` - Stop exporting; files already written are kept
* `test_data_export` - Write a test file with the stored credentials
* `run_data_export` - Export now instead of waiting for the schedule
* `list_data_export_runs` - Recent runs with rows, files, bytes and errors

Credentials are never returned. `create_data_export` and `update_data_export` accept storage credentials and are not available on the OpenAI-reviewed MCP surface.

## Products & Digital Delivery

* `list_products` - List synced products (Stripe, Shopify, WooCommerce, api) including any attached delivery file. Returns one page; supports `limit` and `offset`, and reports `pagination.total` and `pagination.hasMore` so you can tell a truncated page from the full catalog
* `upsert_products` - Create or update products in the catalog (Commerce API, keyed by your `productId`, up to 100 per call)
* `delete_product` - Delete a product previously pushed via `upsert_products`
* `attach_product_file` - Attach a distributable file to a product, delivered after purchase. Pass `url` for a hosted file, or `filePath` to upload a local file (local MCP server only)
* `remove_product_file` - Remove the attached file from a product
* `sync_products` - Queue a sync of the Stripe product catalog

After a file is attached, 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).

## Image Assets

* `upload_image_asset` - Upload a PNG, JPEG, GIF, or WebP image (up to 10MB) to
  the selected company's shared media library. The tool returns the hosted
  `asset.url` and a complete `imageBlock` that can be inserted into campaign,
  sequence, saved-template, or transactional-email blocks.

Use `filePath` when the local stdio MCP server runs on the same machine as the
image. For a remote MCP connector, use `imageBase64` plus `filename` when the
client can expose the attachment bytes. This keeps binary image data out of
`create_sequence` and `update_sequence` while still making the resulting URL
available to every block-based email surface.

For a responsive lifecycle screenshot, set `displayWidthPercent: 100`. Add
`cropHeight` with `objectFit: "cover"` for a fixed-height centered crop, or use
`"contain"` when the full screenshot must remain visible. `altText` is stored
with the media asset and returned as `imageBlock.alt`.

For product walkthrough images with focus rings, arrows, and drop shadows, use
the [Product Screenshot Workflow](/guides/screenshot-workflow) to capture and
annotate the screen first, then upload the generated PNG here.

```json theme={null}
{
  "filePath": "/Users/me/Desktop/product-results.png",
  "altText": "Product results dashboard",
  "displayWidthPercent": 100,
  "cropHeight": 320,
  "objectFit": "cover",
  "align": "center"
}
```

Then copy the returned `imageBlock` into the exact email step's `blocks` array
with `update_sequence` (or the corresponding campaign/template tool). Image
upload alone never changes or sends an email.

## Tags

* `list_tags` - List all tags in the account
* `create_tag` - Create a tag definition. Names are normalized to lowercase with hyphens (`VIP Customer` becomes `vip-customer`); color defaults to `gray`
* `update_tag` - Update a tag's color; system tags cannot be changed
* `delete_tag` - Permanently delete a tag and remove it from all subscribers; system tags and tags used by sequences cannot be deleted

## Templates

* `list_templates` - List email templates newest first, with localization status by locale. Pass `isTemplate: true` to list only reusable master designs. Templates are the company's saved email bodies - standalone templates plus the bodies behind campaigns and transactional emails - so dashboard-designed emails appear here, and a campaign's `emailId` points at its entry. Returns 50 per call by default (`limit` accepts up to 100); page with `offset` while `pagination.hasMore` is true, and read `pagination.total` for the full count
* `get_template` - Get a template's details, content, and localized variants. `isTemplate` is true for reusable master designs
* `create_template` - Create a template from a prompt, raw HTML, or Sequenzy blocks. Pass `isTemplate: true` to save it as a reusable master design that sequence steps and campaigns start from (always as an independent copy)
* `create_template_from_example` - Remix a public email gallery email into a new template: the example's exact layout with the text rewritten for your brand and your logo, color and links swapped in, without naming the example brand
* `update_template` - Update template metadata, inbox preview text, raw HTML, Sequenzy blocks, labels, or the `isTemplate` mark
* `set_template_localization` - Create or replace a caller-supplied localized variant. Pass `keepEdits: true` to protect it like a dashboard edit: automatic on-save translation then keeps it and marks it `stale` when the original changes
* `sync_template_localizations` - Queue AI translation for selected or all enabled non-primary locales. Pass `skipEdited: true` to keep edited variants; they come back in `skippedLocales`
* `share_template` - Create (or fetch) the public "view in browser" link for an individual email - a transactional email (by ID or slug), a sequence email (by the step's `emailId`), or a standalone template. The hosted page renders an anonymized copy - sample contact, inert unsubscribe link, no open/click tracking. Idempotent: an already-active link is returned with `created: false`. `get_template` reports the current link as `shareUrl`. Campaign links follow the A/B winning variant, so share campaigns with `share_campaign` instead
* `unshare_template` - Revoke the public link; the shared URL returns 404 immediately, and sharing again later mints a different URL

Creating a standalone copy of a saved design and AI rewriting within its layout are currently
dashboard-only; MCP exposes no standalone-template copy or fill tool. This is a
[documented interface exception](/concepts/email-templates#availability-across-interfaces).
`create_template` with `prompt` generates new content without preserving an
existing layout. Supplied blocks or HTML create a new body; localized variants
must be supplied separately.

For campaign copies, the existing `create_campaign` tool accepts `templateId`;
it cannot be combined with `prompt` for an AI rewrite.

## Email Blocks

Every tool that writes email content - `create_campaign`, `create_sequence`, `create_template`, `update_sequence_node`, `create_email_component` - takes a `blocks` array. That parameter is declared as a plain object array because the block schema is a large union that would be larger than the rest of the tool schema put together, so this tool is where you look the shapes up.

* `get_email_block_schema` - Required and optional fields for each block type, the allowed values of every enum field, and the shape of nested item arrays and nested objects. Omit `blockType` to list every type; pass one for its full reference plus a minimal valid example and authoring notes. Pass `creatableOnly: true` to hide structural types such as `group` that the editor manages. `conditionFields`, the per-field table for block conditions - which operators each condition field accepts, how its `value` is shaped, and what a preview needs to evaluate it - comes back when you list every type and when you ask for `conditional-group`; on any other single type pass `conditionFields: true`, since the table is several times the size of one block's reference

Call it before authoring a block type you have not written before. If `get_email_block_schema` is not in the MCP tool list, enable it on the Sequenzy connector rather than inferring field names from live HTML; the same reference is also the `sequenzy://email-blocks` resource, `GET /api/v1/email-blocks`, and `sequenzy blocks <type>`. The one shape worth knowing without looking it up: lists are their own block type rather than a text variant.

Video blocks accept an optional `thumbnailUrl`. Block updates replace the supplied block array, so omit `thumbnailUrl` to restore YouTube's own still; `videoUrl` remains 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"
}
```

```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`, so `{"type": "text", "variant": "numbered"}` is rejected. Use `list` for a plain numbered or bulleted list, and `steps` for a visual numbered walkthrough with a title and description per step. List items carry `content`; steps items carry `title` and an optional `description`.

Configuration nested inside a block is described the same way. A field of type `object` carries its own `fields`, so `get_email_block_schema` with `blockType: "repeat"` is where you read that `productSource` takes `strategy` (`personalized`, `bestsellers`, `newest`, `recently_viewed`) alongside `mode` - see [Product Recommendations](/concepts/product-recommendations).

When a block write is rejected, the error names the block type it was validated against, that type's required and optional fields, the shape of the nested array entry or object that failed, and any field you sent that the type does not accept - so a rejection is usually fixable without a second lookup.

## 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 is how you change the footer everywhere at once - editing one email's blocks only changes that email.

* `get_default_email_component` - Read the company's default component for a slot. Call this before changing the footer, then edit the blocks it returns
* `set_default_email_component` - Create or replace the default component for a slot. Replaces the whole component, so read it first when you only mean to change part of it
* `list_email_components` - List reusable components newest first; pass `defaultsOnly` to see only the pinned defaults
* `get_email_component` - Get one component's blocks, `version`, and default-slot status
* `create_email_component` - Save a block group as a reusable component. Names are unique per company
* `update_email_component` - Update metadata or replace blocks. Replacing blocks bumps the component `version`
* `delete_email_component` - Delete a component. Emails that already use it keep their copied blocks

A default footer always keeps its unsubscribe link enabled; transactional sends hide it at render time. Saving without application options changes only future emails. Use the preview workflow below to update existing emails. Raw HTML emails are the exception: block chrome cannot be injected into raw HTML, so they keep their own markup and a generated unsubscribe footer is appended at send time unless the HTML already contains `{{unsubscribeUrl}}`.

## A/B Tests

* `list_ab_tests` - List A/B tests and variants, optionally scoped by sequence
* `get_ab_test` - Get effective settings, variants, content, and localization status. Copy the returned `settings` object; sequence `testPercentage: 100` and `testDurationMinutes: 0` values are legacy sentinels
* `get_ab_test_stats` - Get aggregate and per-variant stats, plus `significance`: whether the leading variant's advantage on the winning metric is statistically significant at 95% confidence
* `select_ab_test_winner` - Select a campaign test winner and queue the winning variant for the remaining audience, which starts external email delivery. On a sequence test, select or change the winner, including one picked automatically, for everyone who reaches the step afterwards; sequence tests need `sequences:write` instead of `campaigns:send`, and `confirmLiveChange: true` while the sequence is active
* `resume_ab_test` - Clear a sequence test's winner so contacts are split across the variants again, keeping the results so far. The test then waits for `select_ab_test_winner` instead of picking automatically; `update_ab_test` with `autoSelectWinner: true` turns automatic selection back on. Pass `confirmLiveChange: true` while the sequence is active
* `create_ab_test` - Provide exactly one of `campaignId` or `automationNodeId`. Campaign tests use `testPercentage`, `testDurationMinutes`, and `winnerCriteria`; a sequence email node is converted to `action_ab_test` with `testType`, `winnerThreshold`, and `winnerCriteria`. An explicit sequence `winnerCriteria` overrides the test-type default. Pass `confirmLiveChange: true` when converting a node in an active sequence. Control variant A is created automatically
* `update_ab_test` - Update campaign or sequence settings. Campaign tests accept percentage/duration/criteria; sequence tests accept type/threshold/criteria and require `confirmLiveChange: true` for active or already-used tests
* `add_ab_test_variant` - Add a variant to a draft campaign or sequence A/B test; sequence variants receive independent email templates
* `update_ab_test_variant` - Update a variant's subject, preview text, HTML, or blocks. Campaign variants are editable in draft only; sequence variants stay editable, with `confirmLiveChange: true` once the sequence is active or the test has activity
* `delete_ab_test_variant` - Permanently remove a variant from a draft campaign or sequence A/B test; variant A is the control and cannot be deleted
* `delete_ab_test` - Permanently delete a campaign A/B test and all of its variants; running tests cannot be deleted

Use `get_sequence` (or `get_ab_test`) to discover variant IDs before editing. `update_ab_test_variant` accepts either `html` or `blocks`, not both. Campaign tests can only be changed while in draft; sequence tests keep sending over time and stay editable, but an edit after the test has started requires `confirmLiveChange: true` and can make combined results inaccurate. Campaign creation requires a draft or rejected campaign; sequence creation requires an `action_email` node. Variants can only be added or removed while the test is in draft status. When a sequence test's parent sequence is active, `add_ab_test_variant` and `delete_ab_test_variant` also require `confirmLiveChange: true` because they immediately change the live rotation.

### Editing the content of a sequence A/B step

Converting a sequence email step with `create_ab_test` moves that step's copy off
the node and onto the test's variants. From then on:

* `get_sequence` still lists the step in `sequence.emails` with `nodeType: "action_ab_test"`, but its `subject`, `previewText`, and `blocks` are **control variant A only**. With `ab_tests:read`, the step's `abTest` object carries the test id, status, and one entry per variant **including that variant's full `blocks`**, plus a `contentEditing` pointer at the tools below. Without that scope, test-record fields are redacted and `variants` is empty while the configured id and editing guidance remain available.
* `update_sequence_node` cannot change variant content. It still edits the step's identity fields (label, sender and reply profile, cc/bcc, transactional flag), and rejects `blocks`/`subject`/`previewText` with a pointer at `update_ab_test_variant`.
* A change that should apply to the whole step has to be repeated on every variant - otherwise you are changing what the test measures.
* Never rebuild an `action_ab_test` step as a plain email node to reach its content. That destroys the test and its results.

The full read-and-update workflow requires `ab_tests:read`, `ab_tests:write`, and `sequences:write` on the API key. The variant edit itself requires both write scopes. **Safer agent access** includes all three. A key holding only the sequence scopes sees the redacted A/B summary from `get_sequence`; calling an A/B tool returns a 403 naming the missing scope, which can be added to the key you are already using. If `update_ab_test_variant` is missing from the MCP tool list, enable it on the Sequenzy connector rather than writing through `update_template` or `update_sequence_node`.

```text theme={null}
get_sequence          -> step.nodeType === "action_ab_test", step.abTest.variants[].blocks
update_ab_test_variant -> apply the change, once per variant
```

`get_ab_test` is still the dedicated A/B read for settings, localization, and stats. A sequence-only MCP allowlist can audit variant copy from `get_sequence` alone.

When you need recipient-specific content, prefer Sequenzy blocks instead of raw HTML. Every block accepts a `conditions` array so it renders only when its rules match. To branch on a value passed in a transactional send's `variables` or an automation `event` payload, use `field: "variable"`:

```json theme={null}
{
  "name": "Launch",
  "subject": "Big update",
  "blocks": [
    {
      "id": "intro",
      "type": "text",
      "content": "<p>Hello there</p>",
      "variant": "paragraph"
    },
    {
      "id": "pro-offer",
      "type": "text",
      "content": "<p>Your Pro upgrade is ready.</p>",
      "variant": "paragraph",
      "conditions": [
        {
          "id": "c1",
          "field": "variable",
          "operator": "is",
          "value": "plan:pro"
        }
      ]
    }
  ]
}
```

For 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` (the value is the plain comparison string). Operators: `is`, `is_not`, `contains`, `not_contains`, `gt`, `gte`, `lt`, `lte`, `is_empty`, `is_not_empty`.

Conditions can also read stored subscriber state - `tag`, `segment`, `list`, `status`, `event`, purchases, and engagement - and each of those fields accepts only its own operators. A tag condition is written `{ "field": "tag", "operator": "contains", "value": "extended" }`; `operator: "is"` is rejected for `tag`, and the rejection names the operators that field does take. `get_email_block_schema` returns the whole table as `conditionFields`: the operators per field, the shape of each field's `value`, and what a preview needs before it can evaluate that field. Listing every type includes it, as does asking for `conditional-group`; for any other single type pass `conditionFields: true`.

Stored-state conditions are evaluated per recipient at send time, so `render_email` can only evaluate them for a stored `subscriberId`, or - for a `tag` condition - an inline `subscriber` that carries `tags`. Anything it cannot evaluate renders as false and comes back in `unevaluatedConditions`, so check that array before treating a preview as proof of which branch a recipient gets.

```json theme={null}
{ "field": "tag", "operator": "contains", "value": "extended" }
```

For if/else, use a `conditional-group` block with `ifBranch` and `elseBranch`:

```json theme={null}
{
  "type": "conditional-group",
  "conditions": [
    { "id": "c1", "field": "variable", "operator": "is", "value": "plan:pro" }
  ],
  "ifBranch": {
    "children": [
      {
        "type": "text",
        "content": "<p>Pro perks inside.</p>",
        "variant": "paragraph"
      }
    ]
  },
  "elseBranch": {
    "children": [
      {
        "type": "text",
        "content": "<p>Upgrade to Pro.</p>",
        "variant": "paragraph"
      }
    ]
  }
}
```

## Team

* `list_team_members` - List the company owner, members with their roles, and pending or expired invitations
* `invite_team_member` - Invite a teammate as `admin`, `marketer`, `viewer`, or `restricted`. Marketers create, edit, and send campaigns and sequences and manage subscribers but cannot access transactional emails, settings, billing, or the team. Existing Sequenzy users are added immediately; everyone else receives an email invitation. Billing access (`canManageBilling`) can only be granted by the company owner and is not available for marketer or restricted members
* `cancel_team_invitation` - Cancel a pending invitation; accepted invitations cannot be cancelled

## Inbox

* `list_conversations` - List subscriber reply conversations with status, search, unread, and pagination filters
* `get_conversation` - Get a conversation with its full message history, subscriber details, and context
* `reply_to_conversation` - Send an outbound reply (requires `bodyText` or `bodyHtml`) or add an internal team-only note with `type: "note"`. Pass `senderProfileId` (from `list_sender_profiles`) to reply from a specific verified sender. Replying to a closed conversation reopens it
* `update_conversation_status` - Open or close a conversation
* `bulk_update_conversation_status` - Open or close up to 100 conversations in one call with `conversationIds` and `status`. Returns `updatedIds`, `unchangedIds` (already had the status) and `notFoundIds`; retrying is safe because only conversations whose status changes are updated
* `mark_conversation_read` - Mark all unread inbound messages in a conversation as read

## Webhooks

* `list_webhooks` - List outbound webhook endpoints and their subscribed event types
* `create_webhook` - Create an endpoint and return its one-time signing secret on the standard MCP surface. It is omitted from the OpenAI-reviewed surface; create it in the dashboard or local CLI there
* `update_webhook` - Update a webhook's name, URL, subscribed events, or enabled/disabled status; providing `events` replaces the existing list
* `delete_webhook` - Permanently delete a webhook endpoint and stop all deliveries to it
* `test_webhook` - Send a test event to verify the endpoint is reachable and signatures validate
* `list_webhook_deliveries` - List recent delivery attempts, including status and response codes
* `replay_webhook_delivery` - Re-send a previous delivery's event payload to the endpoint

Webhook event types cover `email.*`, `sms.*`, `subscriber.*`, and `sequence.*` events.

## Analytics

* `get_stats` - Overview stats (7d/30d/90d). Counts are a funnel over the sends made inside the period: `opened` and `clicked` are unique per email send (not total open events) and include engagement that arrives after the period ends, so they never exceed `sent`. Rates divide by `rateDenominator` (`delivered`, falling back to `sent`), returned with `rateDenominatorBasis`. Also returns top-level `subscriberCount` (every stored contact) and `activeSubscriberCount` (status=active) as a live audience snapshot, independent of period. Set `emailType` to `transactional` for Send API and transactional SMTP open/click rates, including direct and saved-template sends
* `get_transactional_stats` - Aggregate metrics for one saved transactional email by ID or slug, all-time or within a requested period/range
* `list_email_metrics` - One row per email across the account: each campaign and each sequence email step, with its own funnel, attributed conversions, and `revenueCents`. This is the cross-sequence tool: "how many step-4 emails went out across these sequences" is one call with `step: 4` plus the returned `totals`, not one `get_sequence_stats` per sequence. Sequence rows carry `sequenceId`, `sequenceName`, `automationNodeId`, and `step`. Filter by `emailType`, `sequenceId`, `campaignId`, and `step`; sort with `sort`/`order`; page with `page`/`limit`. `totals` covers every matching email, not just the returned page. Counts come from retained event storage, so unlike `list_email_sends` they are not limited to 14 days; omit `period`/`start`/`end` for all-time counts
* `list_email_sends` - Search and filter the recent dashboard delivery history by subject/title, recipient, status, type, or source ID, including `automationNodeId` to list the recipients of one sequence step. Each row carries `recipientEmail`, `subscriberId`, `automationNodeId`, `abTestVariantId`, and the sent/delivered/opened/clicked timestamps, so rows join into a recipient-level delivery matrix without re-reading the raw event stream. Rows are retained for 14 days, so use `get_sequence_stats` `steps[]` or `list_email_metrics` for send totals
* `get_email_send` - Inspect one delivery's status, timestamps, failure details, stored HTML, and event timeline. Failed test sends appear here too, flagged `isTestEmail: true` - a test send is queued rather than delivered inline, so this is where you find out one never arrived
* `list_recipient_suppressions` - Every associated recipient the workspace currently cannot reach, newest first, with a stable `suppressionType`, the reason, the scope (`global` blocks every workspace, `company` only this one), and whether the entry can be removed. Global invalid-recipient rows, company hard bounces without conclusive invalid-inbox evidence, and complaints are protected; company soft-bounce escalations are removable. Start here for "why didn't this person get my email?": this is the standing list of who is blocked, whereas `list_email_sends` shows individual attempts (a suppressed send does get a row there, with status `suppressed`). Filter with `search`, page with `page`/`limit`, and order with `sort` (`suppressedAt`, `email`, `status`) plus `order` (`asc`/`desc`) - `sort=status` surfaces the removable escalations first, and the response echoes the `sortBy`/`sortOrder` actually applied
* `get_recipient_suppression` - Full suppression check for one exact address, including the regional Amazon SES account-level list
* `remove_recipient_suppression` - Clear a company-scoped soft-bounce escalation for one associated address and reactivate the matching bounced subscriber. Global invalid-recipient and Amazon SES account-level suppressions, spam complaints, and unsubscribes are protected
* `get_campaign_stats` - Campaign performance, plus a top-level `clickedLinks` array with the per-link click breakdown, a top-level `goals` array when conversion goals are attached to the campaign, and a top-level `polls` array with answer distributions and NPS score/breakdown when the campaign collected survey responses
* `get_email_client_stats` - Mail client (Apple Mail, Gmail, Outlook, ...) and device shares of unique opens, for one campaign with `campaignId` or company-wide for a period
* `list_poll_responses` - Individual Poll and NPS responses for a campaign: each respondent's email, their answer and stored value, the attribute the answer was saved to, and the response time, newest first. Scope to one block with `blockId`. Only each subscriber's latest answer per block is returned, so counts match `get_campaign_stats`. Use this instead of scanning subscribers for the poll attribute - the attribute carries no response time and reflects the latest answer to any email
* `get_sequence_stats` - Sequence performance, including a `steps` array with each email step's own sent/delivered/opened/clicked/replies counts, node ID, and subject - read that instead of counting `list_sequence_events`. Also returns live active/waiting enrollment-run counts grouped by current node; historical date filters do not limit the live counts. Use `list_sequence_enrollments` to get the actual contacts behind those counts, and `list_email_metrics` to compare the same step across sequences
* `list_sequence_events` - Paginated raw per-recipient events for a sequence's email steps, or one step via `automationNodeId`. Use it to see who received or engaged with a step, not to count sends
* `get_subscriber_activity` - Individual activity

## AI Generation

* `generate_email` - Create email blocks from a prompt. Company logo/footer branding is included by default; use `applyBranding: false` for raw content blocks, or `emailType: "transactional"` for a footer without an unsubscribe link
* `generate_sequence` - Deprecated compatibility alias for goal-based `create_sequence`; it persists the same disabled draft
* `generate_subject_lines` - Generate A/B subject variants
* `generate_sms` - Generate SMS message variants with encoding and segment counts

Generated email content is returned as draft content for review.
`create_sequence` persists a disabled draft automation that appears in
`list_sequences`; the deprecated `generate_sequence` alias does the same.
Email and sequence generation include the company's configured email branding by
default. Sequence generation supports up to 10 emails.

## SMS

* `get_sms_settings` - SMS add-on status: enabled, plan eligibility, credit balance, brand prefix, numbers, and a `readyToSend` flag that accepts either a paid plan or SMS credits plus an active number
* `update_sms_number_label` - Update an SMS number's label and/or its brand prefix override
* `get_sms_usage` - Per-number usage: sends, delivered, failed, credits charged, last sent, and test-send counts
* `send_test_sms` - Send a real test text (charges credits, max 100 per company per rolling 24 hours, bypasses quiet hours); optionally pick which active number it sends from
* `release_sms_number` - Release a toll-free number back to the carrier and free its slot under the workspace's 100-number cap. IMPORTANT: this is irreversible - only call it when the user explicitly asks to remove or release a number

Sequences support native SMS steps: pass `type: "sms"` with a plain-text `text` field in `create_sequence` steps or `insert_sequence_step`, and edit existing SMS steps with `update_sequence.smsSteps` (targeted by `action_sms` nodeId). Check `get_sms_settings` first and warn the user when SMS is not ready - steps added early are stored but skip at runtime until the add-on is enabled and a number is verified.

## Push Notifications

* `get_push_settings` - Web push, APNs and Firebase readiness, last credential errors, and active device counts per platform
* `update_web_push_settings` - Turn web push on or off (the first enable generates VAPID keys) or set the default notification icon
* `set_apns_credentials` / `remove_apns_credentials` - Save or remove the APNs auth key (.p8) for iOS push
* `set_fcm_credentials` / `remove_fcm_credentials` - Save or remove the Firebase service account for Android push
* `list_push_devices` / `register_push_device` / `remove_push_device` - Manage registered browsers and app installs; tokens are never returned in full
* `send_test_push` - Send a real test push to one device or a contact's devices (max 200 per company per rolling 24 hours, excluded from stats)
* `list_push_campaigns`, `get_push_campaign`, `create_push_campaign`, `update_push_campaign`, `duplicate_push_campaign` - Manage push campaign drafts
* `estimate_push_campaign_recipients` - Count audience contacts with an active device
* `send_push_campaign`, `unschedule_push_campaign`, `cancel_push_campaign` - Send, schedule, unschedule, or stop a push campaign. Confirm with the user before sending
* `get_push_campaign_stats` - Sent, displayed, clicked, failed and skipped counts with reasons

Sequences support push steps: pass `type: "push"` with `title` and/or `body` (plus optional `url`, `imageUrl`, `iconUrl`, `platforms`, and `ineligibleAction`) in `create_sequence` steps or `insert_sequence_step`, and edit existing push steps with `update_sequence_node`. Check `get_push_settings` first: push steps and campaigns skip contacts until at least one platform is set up. `set_apns_credentials` and `set_fcm_credentials` are not available on the OpenAI-reviewed surface because they take private keys. See [Push notifications](./push-notifications).

## Feedback

* `submit_feedback` - Send product feedback about Sequenzy to the Sequenzy team

Agents should call `submit_feedback` only when the user explicitly asks to send product feedback to Sequenzy. Categories: `missing_capability`, `bug`, `docs`, `ux`, `praise`, `other`. Every submission goes straight to the team.

The standard surface can include the structured reproduction fields
`userIntent`, `toolCalls`, `expected`, `actual`, and `resourceIds` when they are
needed to investigate a user-requested report. The OpenAI-reviewed surface
accepts only the message, category, and optional generalized workflow context,
and rejects feedback text that contains an email address or resource ID.
Never include unrelated subscriber data, email content, raw API payloads,
debug data, secrets, or API keys.

## Interactive email and sequence previews

In clients that support MCP Apps, including compatible ChatGPT and Claude connections, you can review your saved emails and sequences directly in the conversation.

* `render_email` opens the rendered email with **Desktop** (640px) and **Mobile** (375px) views, its subject, preview text and personalization warnings. Pass a campaign ID, template ID, or sequence ID plus node ID using the existing tool inputs. It renders your saved content through Sequenzy's email renderer. Personalization and locale options remain available through the same tool.
* `preview_sequence({ companyId, sequenceId })` opens the saved sequence as a connected diagram with triggers, delays, branches and actions. Select an email to render it, switch between available A/B variants, or use the **Emails** tab to browse the sequence's emails. Random splits show their configured allocation. **Refresh** reloads saved content, and failed requests offer an explicit retry.

Supporting workflows use compact cards in the same Sequenzy style:

* `open_image_upload({ companyId })` opens an image picker. Choose a PNG, JPEG, GIF or WebP up to 10 MB, optionally describe it, then select **Upload image**. Opening the widget does not write anything. Submission uses `upload_image_asset` with your existing permissions and returns the saved asset to your assistant. After an uncertain failure, check your image library before retrying; uploads are never automatically retried.
* `monitor_subscriber_import({ companyId, importId })` shows counts and progress for an existing import. It polls `get_subscriber_import` every five seconds while running, for up to five minutes. Completion, blocked status, errors or leaving the view stop polling. **Refresh status** restarts monitoring. This does not start or retry imports.

Both widgets pin the company explicitly. Clients without MCP Apps can use `upload_image_asset` and `get_subscriber_import` directly; the same operations are available through the existing REST API and CLI.

The sequence preview requires an explicit company ID from `get_account` and keeps every email render pinned to that company. Previews never activate sequences, enroll subscribers or send email. A/B previews require the existing access to variant content; unavailable variants are shown as unavailable instead of being replaced with the control email.

Email content is isolated from the widget. Scripts, forms, nested frames and link navigation are disabled; external stylesheets and fonts are not loaded. HTTPS email images may load from their original hosts, subject to the client's policy. The widget itself bundles its logo and font and uses authenticated MCP tool calls without exposing credentials. Actual email-client rendering can differ, so use test sends for final inbox verification.

Clients without MCP Apps still receive the existing rendered HTML or sequence data. The same workflows are available through the existing public render and sequence-read endpoints and CLI render/sequence commands. No new REST endpoint or CLI command is needed for this presentation layer.

## Resources

MCP also provides read-only resources that AI can access:

| Resource | Description |
| - | - |
| `sequenzy://dashboard` | Live 7-day overview stats |
| `sequenzy://company` | Currently selected company, including localization settings |
| `sequenzy://campaigns/recent` | Last 10 campaigns |
| `sequenzy://subscribers/recent` | Recently added subscribers |
| `sequenzy://sequences` | All sequences with status |
| `sequenzy://templates` | Available email templates with localization status |
| `sequenzy://segments` | Defined segments |
| `sequenzy://tags` | All tags with counts |
| `sequenzy://email-blocks` | Field reference for every email block type |

## Workflows

### Event-personalized sequences

When your AI assistant creates a sequence with `trigger: "event_received"`, it can place `{{event.*}}` merge tags directly into step subjects or body content.

Those values come from the `properties` payload you send to `POST /api/v1/subscribers/events`.

Example:

* Trigger payload: `{"event":"weather.wind_alert","properties":{"city":"Tel Aviv","alert":{"maxSpeed":75}}}`
* Sequence subject: `Wind alert for {{event.city}}`
* Sequence body: `Winds may reach {{event.alert.maxSpeed}} km/h.`

Sequenzy stores that event snapshot on the sequence run, so delayed follow-up emails keep using the same original event values.

### Apply a default footer to existing emails

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 `preview_default_email_component` with `slot: "footer"`, `blocks`,
`application: { scopes: ["sequences", "campaigns", "transactional", "templates"],
includeCustomized: false }`, and `renderPreview: true`. Inspect
`renderedPreview.footerHtml` and `renderedPreview.samples[].beforeHtml/afterHtml`.
Pass the identical inputs and `previewToken: application.token` to
`set_default_email_component`. You can also supply `sample: { id, kind }` to
preview an affected version. Omitting `application` keeps future-only saves.

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

For running campaign A/B tests, use `get_ab_test` for progress, `update_ab_test` for audience percentage and total duration from the original start, and `select_ab_test_winner` to finish early. Retry the same sample target after an interrupted request; `cancelSampleUpdate: true` discards only a failed sample change. Repeat the same winner ID to repair delivery.

## Example conversations

### Set up email for a new SaaS

```
You: "Set up email sequences for my SaaS that has a 14-day trial"

Claude: I'll help set that up. First, let me check your current sequences...
[Uses list_sequences]

You don't have any sequences yet. I'll create three for you:
1. Welcome sequence (triggered on signup)
2. Trial nurture (days 1, 3, 7, 10, 13)
3. Trial ending reminder (day 12)

[Uses create_sequence for each]

Done! I've created 3 sequences with 8 emails total.
```

### Add a subscriber

```
You: "Add john@example.com as a VIP subscriber"

Claude: [Uses add_subscriber]
Added john@example.com with the VIP tag.
```

### Publish a landing page

```
You: "Create a landing page for our product waitlist and publish it"

Claude: [Uses create_landing_page with template "waitlist", then publish_landing_page]
Done. I created and published the waitlist page. The public URL is ready, and I can connect pages.example.com if you want a branded domain.
```

### Create a Stripe product segment

```
You: "Create a segment for subscribers who bought Stripe product prod_pro at least 3 times"

Claude: [Uses create_segment with filters:
{"id":"filter-1","field":"stripeProduct","operator":"at_least","value":"prod_pro:3"}]
Done. I created the segment and saved it for future campaigns.
```

### Create a match-any segment

```
You: "Create a segment for VIP contacts or anyone on Gmail"

Claude: [Uses create_segment with:
{"filterJoinOperator":"or","filters":[{"id":"filter-1","field":"tag","operator":"contains","value":"vip"},{"id":"filter-2","field":"email","operator":"contains","value":"@gmail.com"}]}]
Done. I created a segment that matches either condition.
```

### Check performance

```
You: "How did my last campaign perform?"

Claude: [Uses list_campaigns then get_campaign_stats]
Your "Product Launch" campaign sent yesterday had:
- 2,451 sent
- 67.2% open rate
- 12.3% click rate
That's above your average!
```

### Create a sequence with specific content

```
You: "Create a 3-email welcome sequence with specific content.
Email 1 (immediate): Subject 'Welcome!' - 'Thanks for joining us.'
Email 2 (1 day later): Subject 'Getting Started' - 'Here's how to begin.'
Email 3 (3 days later): Subject 'Pro Tips' - 'Unlock advanced features.'"

Claude: [Uses create_sequence with steps]
Done! I've created the welcome sequence with 3 emails:
1. "Welcome!" - sends immediately
2. "Getting Started" - sends after 1 day
3. "Pro Tips" - sends after 3 days

The sequence is in draft mode. Would you like me to enable it?
```

### Migrate a sequence from another platform

```
You: "I have these React Email templates for my onboarding sequence.
Can you create a sequence that sends them?"

[Pastes HTML content]

Claude: [Uses create_sequence with steps using html field]
Done! I've created your onboarding sequence with the HTML content
you provided. The HTML was automatically converted to Sequenzy's
block format so you can edit it in the visual editor later.

The sequence is ready in draft mode with 3 emails.
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.