openapi: 3.1.0
info:
  title: Sequenzy API
  description: Sequenzy API for managing subscribers, tags, events, sequences, and transactional emails
  version: 1.0.0
servers:
  - url: https://api.sequenzy.com/api/v1
    description: Production server
  - url: https://api.sequenzy.com/v1
    description: Legacy production server alias

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key passed as Bearer token

  headers:
    RetryAfter:
      description: Number of seconds to wait before retrying the request.
      schema:
        type: integer
        minimum: 1
        example: 1

  responses:
    TransientDatabaseUnavailable:
      description: The database was temporarily unavailable. The request may be retried after the delay in Retry-After.
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error: The service could not reach its database for a moment. Retry the request.
            code: SERVICE_UNAVAILABLE

  schemas:
    WebTrackingKey:
      type: object
      description: >-
        A publishable key for the browser tracking SDK. The key ships in the
        customer's page source by design, so it is not a secret; it authorizes
        storefront customer events only and carries an origin allowlist instead
        of scopes.
      properties:
        id:
          type: string
        name:
          type: string
        publicKey:
          type: string
          description: The publishable key, prefixed seq_pk_.
        allowedOrigins:
          type: array
          items:
            type: string
          description: Normalized origins allowed to use this key. Empty means any origin.
        isActive:
          type: boolean
          description: False when revoked. Revoked keys are rejected at ingest.
        unrestricted:
          type: boolean
          description: True when the allowlist is empty, so any site can send events with this key.
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
          description: When the key last authorized an event, or null if it never has. Updated at most every five minutes.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        installSnippet:
          type: string
          description: The exact script tag to paste into every page. Embeds both the key and the workspace id.
        endpoint:
          type: string
          description: Ingest URL the SDK posts events to.
        warning:
          type: string
          description: Present only when the key is unrestricted.
    IntegrationPixelState:
      type: object
      description: >-
        Live storefront tracking pixel state for a Shopify integration, read
        from the store on every call.
      properties:
        success:
          type: boolean
          example: true
        integrationId:
          type: string
        provider:
          type: string
          example: shopify
        shopDomain:
          type: string
          example: example.myshopify.com
        pixel:
          type: object
          properties:
            installed:
              type: boolean
              description: Whether a pixel for this app exists on the store.
            id:
              type: string
              nullable: true
              description: Shopify's pixel ID.
            endpoint:
              type: string
              nullable: true
              description: Callback URL the installed pixel posts events to.
            endpointCurrent:
              type: boolean
              description: >-
                Whether the pixel posts to this account's canonical endpoint or
                the supported Shopify compatibility endpoint.
            configurationCurrent:
              type: boolean
              description: >-
                Whether the endpoint and signed company, shop, and integration
                settings all match the current connection.
            healthy:
              type: boolean
              description: >-
                Installed, current, and confirmed by Shopify. Check this before
                relying on any on-site event.
            error:
              type: string
              nullable: true
              description: >-
                Set when Shopify could not be reached or refused the query.
                Distinct from "not installed" - the state is unknown.
        dependentEvents:
          type: array
          description: >-
            Event names that depend on this pixel. They are confirmed unable to
            arrive only when pixel.error is null and pixel.healthy is false.
          items:
            type: string
        message:
          type: string
    RenderEmailRequest:
      type: object
      additionalProperties: false
      description: >-
        Personalization and output options for a render. Omit every field to
        render for a sample contact with clean links.
      properties:
        subscriberId:
          type: string
          description: >-
            Personalize as this stored subscriber. Mutually exclusive with
            subscriber. The rendered HTML then carries that subscriber's
            details, so this field additionally requires the subscribers:read
            scope.
          example: "sub_abc123"
        subscriber:
          type: object
          description: Personalize as an ad-hoc contact. Mutually exclusive with subscriberId.
          additionalProperties: false
          properties:
            email:
              type: string
              format: email
            firstName:
              type: string
              nullable: true
            lastName:
              type: string
              nullable: true
            customAttributes:
              type: object
              additionalProperties: true
            tags:
              type: array
              description: >-
                Tags this ad-hoc contact carries, used to evaluate tag block
                conditions. Nothing is stored. Without them a tag condition has
                no tags to read and renders as false, so only the else branch of
                a tag split can be previewed. Every other stored-state condition
                still needs subscriberId.
              items:
                type: string
          required:
            - email
        variables:
          type: object
          description: Extra merge variables layered over the contact's attributes.
          additionalProperties: true
        locale:
          type: string
          description: Force a localization locale instead of deriving it from the contact.
          example: "es"
        variantId:
          type: string
          description: Render a specific A/B test variant. Ignored for templates.
          example: "var_abc123"
        tracking:
          type: boolean
          default: false
          description: >-
            Apply the company's auto-UTM link decoration as a real send would.
            Per-send click redirects and the open pixel need a real email send
            record and are never present in a render.

    RenderEmailResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        html:
          type: string
          description: Email-safe HTML document, rendered exactly as it would be sent.
        subject:
          type: string
          description: Subject line with merge tags resolved.
        previewText:
          type: string
          nullable: true
          description: Inbox preview text with merge tags resolved.
        locale:
          type: string
          description: Localization locale the render resolved to.
          example: "en"
        personalized:
          type: boolean
          description: >-
            False means no contact was supplied, so a sample contact was used
            and contact-specific merge tags resolved to empty values.
        trackingApplied:
          type: boolean
          description: Whether auto-UTM link decoration was applied.
        unresolvedMergeTags:
          type: array
          description: >-
            Merge tags that rendered as an empty string. An unrecognized tag and
            a recognized but blank one are identical in the HTML, so this is the
            only way to tell them apart. Empty when every tag resolved.
          items:
            type: object
            properties:
              tag:
                type: string
                description: Tag name as authored, without braces.
                example: "discountCode"
              reason:
                type: string
                enum: [unknown, no_value]
                description: >-
                  unknown - nothing provides this name, so it stays empty for
                  every recipient. no_value - recognized or unverifiable,
                  but blank for this contact. An unknown name is reported even
                  when a default filter supplied text in its place, because
                  that fallback then reaches every recipient while the rendered
                  HTML looks correctly personalized; a recognized name that is
                  merely blank for this contact is not reported when it has a
                  default. A name is only called unknown
                  when the render had a source to check it against. Without
                  the contact's attributes nothing is checkable, since 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,
                  recommendedProducts.* needs a stored subscriberId the
                  catalog has something to recommend for, and discount.* is
                  only checkable on a sequence step whose incoming paths all
                  run the same discount step. Rendering a transactional email
                  is checkable only when variables is passed, since its tags
                  come from the variables of each send call and carry no
                  prefix marking them. Otherwise those tags land in
                  no_value rather than in unknown. 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 key without it may report such a
                  name as unknown.
            required:
              - tag
              - reason
        unevaluatedConditions:
          type: array
          description: >-
            Block conditions this render could not decide. Each was rendered as
            false, the same fail-closed rule a live send uses, so an else branch
            in the HTML is not evidence that the condition is false for a real
            recipient. Empty when every condition was actually evaluated.
          items:
            type: object
            properties:
              field:
                type: string
                description: Condition field, such as tag or segment.
                example: "tag"
              operator:
                type: string
                example: "contains"
              value:
                type: string
                example: "extended"
              description:
                type: string
                description: The condition in the words the dashboard uses.
                example: "Tag contains extended"
              reason:
                type: string
                enum:
                  [
                    requires_stored_subscriber,
                    invalid_filter,
                    evaluation_failed,
                  ]
                description: >-
                  requires_stored_subscriber - the field reads stored subscriber
                  state, so pass subscriberId, or for a tag condition pass tags
                  on the inline subscriber. invalid_filter - the stored
                  condition is malformed, which fails closed on a real send too;
                  hint carries the validation error. evaluation_failed - the
                  lookup itself failed and the render is worth retrying.
              hint:
                type: string
                description: What to change to get this condition evaluated.
            required:
              - field
              - operator
              - value
              - description
              - reason
              - hint
        entity:
          type: object
          properties:
            type:
              type: string
              enum: [campaign, sequence_step, template]
            id:
              type: string
            variantId:
              type: string
              nullable: true
          required:
            - type
            - id
            - variantId
      required:
        - success
        - html
        - subject
        - previewText
        - locale
        - personalized
        - trackingApplied
        - unresolvedMergeTags
        - unevaluatedConditions
        - entity

    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          example: "Error message"
        code:
          type: string
          description: Optional stable machine-readable error discriminator when available.
          example: COMPANY_CREATE_REQUIRES_PERSONAL_KEY
        retryable:
          type: boolean
          description: Whether retrying the request can recover from the error.
      required:
        - error

    SavedPopupTrigger:
      type: object
      description: When the popup opens. Merged key by key into the popup's current trigger.
      additionalProperties: true
      properties:
        type:
          type: string
          enum: [delay, scroll, exit-intent, click, manual]
          default: delay
        delaySeconds:
          type: integer
          minimum: 0
          maximum: 3600
          default: 5
          description: Used when type is delay.
        scrollPercent:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
          description: Used when type is scroll.
        clickSelector:
          type: string
          maxLength: 300
          description: CSS selector of the element that opens the popup. Required when type is click.

    SavedPopupTargeting:
      type: object
      description: Where the popup may show. Empty arrays mean no restriction. Merged key by key.
      additionalProperties: true
      properties:
        domains:
          type: array
          maxItems: 50
          items:
            type: string
        paths:
          type: array
          maxItems: 100
          items:
            type: string
        excludedPaths:
          type: array
          maxItems: 100
          items:
            type: string
        device:
          type: string
          enum: [all, desktop, mobile]
          default: all

    SavedPopupSchedule:
      type: object
      description: Optional run window. Use null on either key to clear it. Merged key by key.
      additionalProperties: true
      properties:
        startsAt:
          type:
            - string
            - "null"
          format: date-time
        endsAt:
          type:
            - string
            - "null"
          format: date-time
          description: Must be later than startsAt.

    SavedPopupFrequency:
      type: object
      description: How often one visitor sees the popup. Merged key by key.
      additionalProperties: true
      properties:
        maxDisplays:
          type: integer
          minimum: 1
          maximum: 100
          default: 3
        windowDays:
          type: integer
          minimum: 1
          maximum: 365
          default: 7

    SavedPopupVisual:
      type: object
      description: Media panel and urgency treatment. Merged key by key.
      additionalProperties: true
      properties:
        style:
          type: string
          enum: [none, accent, header, rail, image, countdown]
          default: accent
        placement:
          type: string
          enum: [top, left, center, right]
          default: top
        imageUrl:
          type:
            - string
            - "null"
          format: uri
        imageAlt:
          type: string
          maxLength: 160
        countdownMinutes:
          type:
            - integer
            - "null"
          minimum: 1
          maximum: 10080

    SavedPopup:
      type: object
      description: A saved on-site signup popup.
      properties:
        id:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [draft, published]
        content:
          type: object
          description: Complete popup content - template, presentation, placement, theme, settings, trigger, targeting, schedule, frequency, visual, and blocks. Present on detail reads; omitted when listing unless `includeContent=true`.
          additionalProperties: true
        viewCount:
          type: integer
          description: Times the popup was shown to a visitor.
        startCount:
          type: integer
          description: Times a visitor began filling the popup in. Always between viewCount and conversionCount.
        conversionCount:
          type: integer
          description: Times a visitor submitted the popup and became a subscriber.
        stats:
          $ref: "#/components/schemas/SavedPopupStats"
        publishedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          description: Dashboard URL for this popup.

    SavedPopupStats:
      type: object
      description: The popup funnel - shown, started, submitted - with rates derived from the raw counters. A rate is null rather than 0 when its denominator is zero, so "nobody has seen it yet" never reads as "nobody converted".
      properties:
        views:
          type: integer
        starts:
          type: integer
        conversions:
          type: integer
        startRate:
          type:
            - number
            - "null"
          description: starts / views, 0-1. Null when the popup has no views.
        conversionRate:
          type:
            - number
            - "null"
          description: conversions / views, 0-1. Null when the popup has no views.
        completionRate:
          type:
            - number
            - "null"
          description: conversions / starts, 0-1. Null when nobody has started it.

    SavedPopupEmbed:
      type: object
      description: Deployment snippets for a published popup. Present only when the popup is published; they contain no API key.
      properties:
        scriptUrl:
          type: string
          format: uri
        javascript:
          type: string
        react:
          type: string
        wordpress:
          type: string
        shopify:
          type: string
        supportedPlatforms:
          type: array
          items:
            type: string

    ApiKeyMetadata:
      type: object
      description: Non-secret metadata for a company-scoped API key. Plain key values and stored hashes are never returned.
      additionalProperties: false
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
          description: Non-secret key prefix for identifying the credential.
        type:
          type: string
          enum: [company]
        scopes:
          type:
            - array
            - "null"
          description: Explicit permission scopes, or null for full access.
          items:
            type: string
        permissions:
          type: object
          description: Effective permission receipt for the key.
          additionalProperties: true
        deviceName:
          type:
            - string
            - "null"
        lastUsedAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        isCurrent:
          type: boolean
          description: Whether this is the credential authenticating the request.
      required:
        - id
        - name
        - prefix
        - type
        - scopes
        - permissions
        - deviceName
        - lastUsedAt
        - createdAt
        - updatedAt
        - isCurrent

    TransactionalSendDiagnostics:
      type: object
      description: Non-blocking warnings about template variable issues. The send is still queued when this object is present, and missing values without defaults render as empty strings.
      properties:
        status:
          type: string
          enum: [warning]
          example: warning
        message:
          type: string
          example: Email was queued. A required variable is missing and will render as an empty string.
        missingRequiredVariables:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                example: RESET_URL
              lookupName:
                type: string
                example: reset_url
              message:
                type: string
                example: Email was queued. Required variable "RESET_URL" is missing and will render as an empty string.
              usedIn:
                type: array
                items:
                  type: object
                  properties:
                    surface:
                      type: string
                      enum: [subject, previewText, body, block]
                      example: block
                    field:
                      type: string
                      example: url
                    blockId:
                      type: string
                      example: reset-button
                    blockType:
                      type: string
                      example: button
              suggestions:
                type: array
                items:
                  type: string
                example: ["resetpasswordlink"]
        unusedVariables:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                example: resetpasswordlink
              message:
                type: string
                example: Variable "resetpasswordlink" was provided but is not used by the email template.
              suggestions:
                type: array
                items:
                  type: string
                example: ["reset_url"]
      required:
        - status
        - message
        - missingRequiredVariables
        - unusedVariables

    CommerceCustomer:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          example: "buyer@example.com"
        externalId:
          type: string
          description: Customer-owned app/customer/user ID
          example: "user_123"
        firstName:
          type: string
          example: "Jane"
        lastName:
          type: string
          example: "Doe"
        attributes:
          type: object
          additionalProperties: true
          description: Custom subscriber attributes to merge into the profile. Synced to ClickHouse for segment filtering.
          example: { "acquisitionChannel": "tiktok", "country": "US" }

    CommerceOrderItem:
      type: object
      required:
        - productId
        - title
        - quantity
      properties:
        productId:
          type: string
          description: Your product identifier (same value used when upserting products)
          example: "SKU-PROTEIN-1KG"
        variantId:
          type: string
          nullable: true
          description: Your variant identifier within the product
          example: "SKU-PROTEIN-1KG-VANILLA"
        sku:
          type: string
          nullable: true
          example: "VANILLA-1KG"
        title:
          type: string
          example: "Protein Powder"
        variantTitle:
          type: string
          nullable: true
          example: "Vanilla"
        quantity:
          type: integer
          minimum: 0
          example: 1
        priceCents:
          type: integer
          nullable: true
          description: Unit price in cents
          example: 8850

    CommerceProduct:
      type: object
      properties:
        id:
          type: string
          description: Internal Sequenzy product ID
        productId:
          type: string
          nullable: true
          description: Your product identifier (providerProductId)
          example: "SKU-PROTEIN-1KG"
        providerProductId:
          type: string
          nullable: true
          description: Provider product identifier
          example: "prod_ABC123"
        provider:
          type: string
          enum: [api, stripe, shopify, woocommerce, manual]
          example: "api"
        title:
          type: string
          example: "Protein Powder"
        description:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        price:
          type: integer
          nullable: true
          description: Unit price in cents
          example: 8850
        priceCents:
          type: integer
          nullable: true
          example: 8850
        compareAtPrice:
          type: integer
          nullable: true
        compareAtPriceCents:
          type: integer
          nullable: true
        currency:
          type: string
          nullable: true
          example: "USD"
        inStock:
          type: boolean
        providerCreatedAt:
          type: string
          format: date-time
          nullable: true
          description: Product creation time reported by the source catalog. Used for newest-product ranking.
        digitalDelivery:
          $ref: "#/components/schemas/ProductDigitalDelivery"
        variants:
          type: array
          description: For Stripe products, every active Stripe price of the product (variantId is the Stripe price ID).
          items:
            type: object
            properties:
              variantId:
                type: string
              title:
                type: string
              sku:
                type: string
                nullable: true
              priceCents:
                type: integer
                nullable: true
              compareAtPriceCents:
                type: integer
                nullable: true
              currency:
                type: string
                nullable: true
                description: The variant's own currency. Stripe products can have active prices in several currencies, so this can differ from the product-level currency.
                example: "USD"
              imageUrl:
                type: string
                nullable: true
              inStock:
                type: boolean
              inventoryQuantity:
                type: integer
                nullable: true
              options:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    value:
                      type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    AgentFriendlyError:
      type: object
      properties:
        success:
          type: boolean
          example: false
        code:
          type: string
          description: Stable machine-readable error code.
          example: SEGMENT_NAME_ALREADY_EXISTS
        error:
          type: string
          description: Short error label.
          example: Segment name already exists
        title:
          type: string
          description: UI-friendly title.
          example: Segment name already exists
        description:
          type: string
          description: Agent-friendly explanation of why the request failed.
          example: A saved segment with this name already exists in this company.
        resolution:
          type: string
          description: Concrete next step for humans or AI agents.
          example: Call list_segments before creating, reuse the existing segment id, or retry with a different name.
        docsUrl:
          type: string
          format: uri
          description: Documentation URL for the failed operation.
          example: https://docs.sequenzy.com/api-reference/segments/create
        details:
          type: object
          additionalProperties: true
      required:
        - success
        - code
        - error
        - title
        - description
        - resolution
        - docsUrl

    EngagementStats:
      type: object
      description: >-
        Unified engagement metrics returned by analytics endpoints. When a period
        or start/end filter is applied these are a funnel over the sends made
        inside that window, not a log of events inside it: every delivery,
        engagement, and reply count is attributed to one of those sends, including
        activity that arrives after the window closes, so opened <= delivered <=
        sent always holds and no rate can exceed 100%. conversions and
        revenueCents are the exception and use the goal's own last-touch
        attribution window. Open and click metrics exclude detected scanner,
        preview, and tracked asset events unless includeMachineEngagement is true.
      properties:
        sent:
          type: integer
        delivered:
          type: integer
          description: "Capped at sent count"
        bounced:
          type: integer
          description: "Unique bounces (deduplicated by email send)"
        opened:
          type: integer
          description: "Unique opens (deduplicated by email send)"
        clicked:
          type: integer
          description: "Unique clicks (deduplicated by email send)"
        replies:
          type: integer
          description: "Inbound replies captured for delivered emails"
        unsubscribed:
          type: integer
        deliveryRate:
          type: number
          description: "Percentage (0-100)"
        bounceRate:
          type: number
          description: "Percentage (0-100), calculated from sent emails"
        rateDenominator:
          type: integer
          description: "The number every engagement rate divides by: delivered, falling back to sent when no delivery events were recorded, and 0 when nothing was sent"
        rateDenominatorBasis:
          type: string
          enum: [delivered, sent, none]
          description: "Which field rateDenominator was taken from"
        openRate:
          type: number
          description: "Percentage (0-100), opened / rateDenominator"
        clickRate:
          type: number
          description: "Percentage (0-100), clicked / rateDenominator"
        replyRate:
          type: number
          description: "Percentage (0-100), replies / rateDenominator"
        unsubscribeRate:
          type: number
          description: "Percentage (0-100), unsubscribed / rateDenominator"
        conversions:
          type: integer
          description: "Attributed goal conversions (last-touch, 24h window). Only returned by campaign and sequence metrics endpoints."
        revenueCents:
          type: integer
          description: "Attributed revenue in cents from purchase events (saas.purchase and ecommerce.order_placed). Only returned by campaign and sequence metrics endpoints."

    SequenceEnrollmentCounts:
      type: object
      description: >-
        Point-in-time counts of active and waiting enrollment tokens for a
        sequence. This live snapshot is not limited by historical period,
        start, or end filters. Counts represent enrollment runs, not
        necessarily distinct subscribers.
      required:
        - active
        - waiting
        - total
        - byCurrentNode
      properties:
        active:
          type: integer
          description: Active enrollment-token count.
        waiting:
          type: integer
          description: Waiting enrollment-token count.
        total:
          type: integer
          description: Total active plus waiting enrollment-token count.
        byCurrentNode:
          type: array
          description: Active and waiting enrollment counts grouped by current sequence node.
          items:
            type: object
            required:
              - currentNodeId
              - currentNodeMissing
              - active
              - waiting
              - total
            properties:
              currentNodeId:
                type: string
                description: Current sequence node ID.
              currentNodeType:
                type: string
                description: Current sequence node type. Omitted when the node no longer exists in the graph.
              currentNodeLabel:
                type: string
                description: Current sequence node label or email subject when available.
              currentNodeMissing:
                type: boolean
                description: Whether the current node no longer exists in the sequence graph.
              active:
                type: integer
                description: Active enrollment-token count at this node.
              waiting:
                type: integer
                description: Waiting enrollment-token count at this node.
              total:
                type: integer
                description: Total active plus waiting enrollment-token count at this node.

    TransactionalMetricsResponse:
      type: object
      properties:
        success:
          type: boolean
        period:
          type: string
        start:
          type: string
          format: date-time
        end:
          type: string
          format: date-time
        transactional:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            slug:
              type: string
        stats:
          allOf:
            - $ref: "#/components/schemas/EngagementStats"
            - type: object
              properties:
                complaints:
                  type: integer
                complaintRate:
                  type: number
                  format: float
        complaints:
          type: object
          properties:
            count:
              type: integer
            rate:
              type: number
              format: float
        clickedLinks:
          type: array
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
              clicks:
                type: integer
              percentage:
                type: number
                format: float
        bounceBreakdown:
          type: object
          properties:
            permanent:
              type: integer
            transient:
              type: integer
            undetermined:
              type: integer
            subtypes:
              type: array
              items:
                type: object
                properties:
                  type:
                    type: string
                    enum: [Permanent, Transient, Undetermined]
                  subType:
                    type: string
                    nullable: true
                  count:
                    type: integer
        engagementBreakdown:
          type: object
          properties:
            human:
              type: object
              properties:
                opens:
                  type: integer
                clicks:
                  type: integer
            machine:
              type: object
              properties:
                opens:
                  type: integer
                clicks:
                  type: integer

    CommerceValueForecast:
      type: object
      description: Explainable commerce forecast derived from up to two years of provider-neutral placed-order events.
      properties:
        status:
          type: string
          enum: [ready, insufficient_data]
        generatedAt:
          type: string
          format: date-time
        modelVersion:
          type: string
          example: cohort-rfm-v1
        currency:
          type: string
          nullable: true
        confidence:
          type: string
          enum: [low, medium, high]
          nullable: true
        isSampled:
          type: boolean
        eligibility:
          type: object
          properties:
            reasons:
              type: array
              items:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  current:
                    type: number
                  required:
                    type: number
        history:
          type: object
          properties:
            orderCount:
              type: integer
            customerCount:
              type: integer
            repeatCustomerCount:
              type: integer
            historyDays:
              type: integer
            daysSinceLastOrder:
              type: integer
            excludedCurrencyOrderCount:
              type: integer
        actual:
          type: object
          properties:
            averageOrderValueCents:
              type: integer
              nullable: true
            averageCustomerValueCents:
              type: integer
              nullable: true
            repeatPurchaseRate:
              type: number
              nullable: true
        forecast:
          type: object
          nullable: true
          properties:
            predictedAverageOrderValueCents:
              type: integer
            predictedAverageOrderValueRange:
              $ref: "#/components/schemas/CommerceForecastRange"
            predictedCustomerValue365DaysCents:
              type: integer
            predictedCustomerValue365DaysRange:
              $ref: "#/components/schemas/CommerceForecastRange"
            expectedOrders90Days:
              type: number
            expectedRevenue90DaysCents:
              type: integer
            expectedRevenue90DaysRange:
              $ref: "#/components/schemas/CommerceForecastRange"

    CommerceForecastRange:
      type: object
      properties:
        lowCents:
          type: integer
        highCents:
          type: integer

    ClickedLink:
      type: object
      description: >-
        One destination URL from the campaign's per-link click breakdown.
        Returned as a top-level `clickedLinks` array by the campaign metrics
        endpoint when the campaign has tracked link clicks.
      properties:
        url:
          type: string
          description: The clicked destination URL as sent in the email.
        clicks:
          type: integer
          description: Number of recorded clicks on this URL.
        percentage:
          type: number
          description: "This link's share of every recorded link click (0-100), including links beyond the returned top 20."

    RecommendationMetrics:
      type: object
      description: >-
        Product recommendation funnel for a campaign or sequence. Returned as
        a top-level `recommendations` object when recommendation blocks were
        rendered. The requested period or custom time range scopes impressions
        and clicks. Orders and revenue are attributed when a subscriber buys a
        recommended product within 7 days of a scoped click.
      properties:
        impressions:
          type: integer
          description: Total recommended products rendered across all sends.
        recipients:
          type: integer
          description: Unique subscribers shown at least one recommendation.
        clicks:
          type: integer
          description: Recorded clicks on recommended products.
        clickers:
          type: integer
          description: Unique subscribers who clicked a recommendation.
        orders:
          type: integer
          description: Orders containing a clicked recommended product.
        revenueCents:
          type: integer
          description: Legacy sum of attributed order minor units across currencies. Use revenueByCurrency for display or financial analysis.
        revenueByCurrency:
          type: array
          description: Currency-safe attributed revenue totals. UNKNOWN identifies orders whose source event omitted currency.
          items:
            type: object
            properties:
              currency:
                type: string
                description: ISO 4217 code or UNKNOWN.
              revenueCents:
                type: integer
                description: Attributed revenue in this currency's minor units.
        topProducts:
          type: array
          description: Per-product impressions and clicks, most clicked first.
          items:
            type: object
            properties:
              provider:
                type: string
                description: Commerce provider (shopify, woocommerce, ...).
              providerProductId:
                type: string
                description: Product ID in the provider's catalog.
              impressions:
                type: integer
              clicks:
                type: integer

    PollResultsSummary:
      type: object
      description: >-
        Aggregated poll / NPS answers for one poll block. Each subscriber
        counts once per block using their latest answer; multi-select polls
        (allowMultiple) count each subscriber once per selected option, so
        answer percentages can sum past 100. Returned as a top-level `polls`
        array by the campaign metrics endpoint when the campaign collected
        poll responses.
      properties:
        blockId:
          type: string
          description: Poll block id inside the email content.
        variant:
          type: string
          enum:
            - options
            - nps
        allowMultiple:
          type: boolean
          description: >-
            Present and true for multi-select polls, where recipients save a
            set of options from the hosted selection page and the attribute
            stores the list of selected values.
        question:
          type: string
        attributeKey:
          type: string
          description: >-
            Subscriber attribute key where the current/latest response is
            stored. A later poll that reuses this key can overwrite it, so use
            a campaign-and-block-scoped pollResponse subscriber filter for an
            exact historical respondent drill-down. Omitted when the recorded
            responses carry no valid key.
        totalResponses:
          type: integer
          description: >-
            Number of respondents (subscribers whose latest answer selects at
            least one option; a cleared multi-select submit counts as no
            answer). Also the percentage denominator.
        answers:
          type: array
          description: Ordered by responses (options) or by score 0-10 (NPS, zero-filled).
          items:
            type: object
            properties:
              answer:
                type: string
                description: Human-readable answer label captured when the response was recorded.
              value:
                type: string
                description: Stable stored answer value. Use this field as the answer identifier when labels can change.
              responses:
                type: integer
              percentage:
                type: number
                description: >-
                  Share of the poll's respondents (0-100, one decimal). Sums
                  can exceed 100 for multi-select polls.
        nps:
          type: object
          description: Present for NPS polls only.
          properties:
            score:
              type: integer
              description: "Net Promoter Score (-100 to 100)"
            average:
              type: number
            promoters:
              type: integer
            passives:
              type: integer
            detractors:
              type: integer

    EmailEvent:
      type: object
      properties:
        id:
          type: string
        emailSendId:
          type: string
        companyId:
          type: string
        campaignId:
          type: string
          nullable: true
          description: Campaign ID for campaigns, or automation email node ID for sequence events.
        transactionalEmailId:
          type: string
          nullable: true
        subscriberId:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
        emailName:
          type: string
          nullable: true
        emailType:
          type: string
          nullable: true
          enum: [campaign, sequence, transactional]
        abTestId:
          type: string
          nullable: true
        abTestVariantId:
          type: string
          nullable: true
        eventType:
          type: string
          enum:
            [
              send,
              delivery,
              bounce,
              complaint,
              open,
              click,
              unsubscribe,
              delivery_delay,
              transport_failure,
            ]
        bounceType:
          type: string
          nullable: true
        bounceSubType:
          type: string
          nullable: true
        complaintType:
          type: string
          nullable: true
        clickedUrl:
          type: string
          nullable: true
        ipAddress:
          type: string
          nullable: true
        userAgent:
          type: string
          nullable: true
        countryCode:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        eventTime:
          type: string
          format: date-time

    PollResponse:
      type: object
      description: One respondent's latest answer to one Poll or NPS block.
      properties:
        subscriberId:
          type: string
        email:
          type: string
          nullable: true
          description: Null when the subscriber has since been deleted.
        externalId:
          type: string
          nullable: true
        firstName:
          type: string
          nullable: true
        lastName:
          type: string
          nullable: true
        blockId:
          type: string
          description: Poll block ID inside the email content.
        variant:
          type: string
          enum: [options, nps]
        question:
          type: string
        attributeKey:
          type: string
          nullable: true
          description: Subscriber attribute the answer was stored under.
        allowMultiple:
          type: boolean
          description: Whether the poll accepted several selections.
        answers:
          type: array
          description: Selected option labels. One entry, or several for a multi-select poll.
          items:
            type: string
        values:
          type: array
          description: Stored values parallel to `answers`.
          items:
            type: string
        respondedAt:
          type: string
          format: date-time

    ShopifyBrowseAbandonmentSettings:
      type: object
      additionalProperties: false
      properties:
        enabled:
          type: boolean
          description: Whether browse-abandonment events fire for this store (default true).
        delayHours:
          type: number
          exclusiveMinimum: 0
          maximum: 168
          description: Hours to wait after a product view before the abandonment check (default 2, max 168).
        cooldownHours:
          type: number
          exclusiveMinimum: 0
          maximum: 720
          description: Minimum hours between browse-abandoned events per subscriber (default 24, max 720).
    ShopifyCartAbandonmentSettings:
      type: object
      additionalProperties: false
      properties:
        enabled:
          type: boolean
          description: Whether cart-abandonment events fire for this store (default true).
        delayHours:
          type: number
          exclusiveMinimum: 0
          maximum: 168
          description: Hours of cart inactivity before the cart counts as abandoned (default 1, max 168).
        cooldownHours:
          type: number
          exclusiveMinimum: 0
          maximum: 720
          description: Minimum hours between cart-abandoned events per subscriber (default 24, max 720).
        expireAfterHours:
          type: number
          exclusiveMinimum: 0
          maximum: 720
          description: "Hours after which an untouched tracked cart is dropped instead of being emailed about or merged into by a later add (default 72, max 720). A store can empty a cart without sending a removal - a session or reservation timing out, an inventory hold releasing, checkout finishing on another device - and past this window the snapshot is no longer treated as evidence the shopper still has those items. Set it below delayHours on stores whose carts expire faster than the abandonment delay: those carts are then intentionally never emailed about."
    ShopifyPriceDropSettings:
      type: object
      additionalProperties: false
      properties:
        enabled:
          type: boolean
          description: Whether price-drop events fire for this store (default true).
        minPercent:
          type: number
          exclusiveMinimum: 0
          maximum: 95
          description: Minimum price decrease percent to alert on (default 5, max 95).
        lookbackDays:
          type: number
          exclusiveMinimum: 0
          maximum: 90
          description: How many days back product viewers qualify as the audience (default 30, max 90).
        cooldownDays:
          type: number
          exclusiveMinimum: 0
          maximum: 90
          description: Minimum days between price-drop events per subscriber and product (default 7, max 90).
    SyncRule:
      type: object
      required:
        - triggerEvent
        - actions
      properties:
        triggerEvent:
          type: string
          description: Event name that triggers the rule.
          example: "ecommerce.order_placed"
        actions:
          type: object
          required:
            - addTags
            - removeTags
          properties:
            addTags:
              type: array
              items:
                type: string
              example: ["vinyl-collector"]
            removeTags:
              type: array
              items:
                type: string
              example: []
        conditions:
          type: object
          description: Optional conditions that must all hold for the rule to apply.
          properties:
            requiresTags:
              type: array
              description: Rule only applies if the subscriber has ALL of these tags.
              items:
                type: string
            requiresNotTags:
              type: array
              description: Rule only applies if the subscriber has NONE of these tags.
              items:
                type: string
            purchasedProduct:
              type: object
              description: For commerce events with product context - rule only applies when a product on the event matches every specified selector (values within a selector are OR'd).
              properties:
                tags:
                  type: array
                  items:
                    type: string
                collectionIds:
                  type: array
                  items:
                    type: string
                productTypes:
                  type: array
                  items:
                    type: string
                vendors:
                  type: array
                  items:
                    type: string

    Company:
      type: object
      properties:
        id:
          type: string
          example: "company_abc123"
        name:
          type: string
          example: "Acme Inc"
        description:
          type: string
          nullable: true
          example: "Lifecycle email automation for SaaS teams."
        status:
          type: string
          example: "ready"
        websiteUrl:
          type: string
          nullable: true
          example: "acme.com"
        logoUrl:
          type: string
          nullable: true
          example: "https://example.com/logo.png"
        founderName:
          type: string
          nullable: true
          example: "Sarah"
        primaryColor:
          type: string
          nullable: true
          example: "#0ea5e9"
        brandColors:
          type: object
          nullable: true
          additionalProperties: true
          example:
            primary: "#0ea5e9"
        valueProps:
          type: array
          nullable: true
          items:
            type: object
            additionalProperties: true
          example:
            - title: "Fast setup"
              description: "Launch lifecycle emails in minutes."
        testimonials:
          type: array
          nullable: true
          items:
            type: object
            additionalProperties: true
        toneVoice:
          type: string
          nullable: true
          example: "clear, direct, warm"
        companyContext:
          type: string
          nullable: true
          example: "Acme helps SaaS teams send lifecycle emails from product events."
        emailDesignPrompt:
          type: string
          nullable: true
          description: >-
            Art direction for AI-designed emails: layout, density, which
            sections belong in an email, imagery, and CTA prominence.
            `toneVoice` steers copy; this steers design. When empty, the
            next email generation prefills it with the direction derived
            from the brand.
          example: "Editorial and calm. One idea per email, no feature grids."
        emailLengthPreference:
          type: string
          enum: [concise, balanced, detailed]
          description: >-
            How long AI-written email copy should be. New workspaces default to
            `concise`.
          example: "concise"
        socialLinks:
          type: object
          nullable: true
          additionalProperties:
            type: string
        privacyPolicyUrl:
          type: string
          nullable: true
        termsUrl:
          type: string
          nullable: true
        address:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          description: Dashboard edit URL for this campaign.
          example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123"
        previewUrl:
          type: string
          format: uri
          description: Dashboard review/preview URL for this campaign.
          example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
        language:
          type: string
          example: "en"
        pricing:
          type: object
          nullable: true
          additionalProperties: true
        fontFamily:
          type: string
          nullable: true
        emailTheme:
          type: object
          nullable: true
          additionalProperties: true
        emailDirection:
          type: string
          example: "ltr"
        emailLocalizationConfig:
          type: object
          additionalProperties: true
        defaultSubscriberListIds:
          type: array
          nullable: true
          items:
            type: string
          description: >-
            Workspace default lists new contacts join when nothing targets
            them explicitly. null means every current and future list, []
            means no list at all, and an array means exactly those lists.
          example: ["list_abc123"]
        defaultSenderProfileId:
          type: string
          nullable: true
        defaultFromName:
          type: string
          nullable: true
        defaultFromEmail:
          type: string
          format: email
          nullable: true
        defaultReplyProfileId:
          type: string
          nullable: true
        defaultReplyToName:
          type: string
          nullable: true
        defaultReplyToEmail:
          type: string
          format: email
          nullable: true
        replyTrackingEnabled:
          type: boolean
          description: Whether inbound reply capture is enabled for this company.
        replyTrackingDomainMode:
          type: string
          enum: [sequenzy, custom]
          description: Whether reply capture uses Sequenzy's managed inbound domain or a configured custom domain.
        forwardReplies:
          type: boolean
          description: Whether captured replies are forwarded to the configured mailbox.
        replyRetentionDays:
          type: integer
          readOnly: true
          description: Current reply retention period in days.

    Website:
      type: object
      properties:
        id:
          type: string
        domain:
          type: string
          example: "mail.example.com"
        status:
          type: string
          enum: [not_started, pending, verified, failed, misconfigured]
          example: "pending"
          description: Stored DNS verification status. This does not imply that a sending transport is ready.
        dnsVerified:
          type: boolean
          description: Whether the customer DNS records are verified.
        readyToSend:
          type: boolean
          description: Whether the domain is fully ready to send (DNS verified and activation finished).
        readiness:
          type: object
          description:
            Customer-facing sending readiness. When readyToSend is false,
            reason says why; dns_* reasons describe your DNS records, while
            activation reasons resolve on Sequenzy's side.
          properties:
            status:
              type: string
              enum: [setup_required, verifying_dns, activating, ready, blocked]
            reason:
              type: string
              nullable: true
              enum:
                [
                  dns_not_started,
                  dns_pending,
                  dns_failed,
                  activation_pending,
                  activation_failed,
                  sending_unavailable,
                ]
        message:
          type: string
          description: Creation or setup summary when returned by a mutating operation.
        dnsRecords:
          type: object
          nullable: true
          additionalProperties: true
          description:
            The DNS records to publish and their per-record verification
            status (DKIM, SPF, DMARC, MAIL FROM).
        dkim:
          type: object
          additionalProperties: true
          description: Normalized DKIM type, status, and diagnostics.
        spf:
          type: object
          additionalProperties: true
          description: Normalized SPF status, expected record, and diagnostics.
        mailFrom:
          type: object
          additionalProperties: true
          description: Normalized custom MAIL FROM MX status and diagnostics.
        lastVerifiedAt:
          type: string
          format: date-time
          nullable: true
        nextVerificationAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    SubscriberList:
      type: object
      properties:
        id:
          type: string
          example: "list_abc123"
        name:
          type: string
          example: "Newsletter Subscribers"
        description:
          type: string
          nullable: true
        isPrivate:
          type: boolean
          description: Whether the list is private. Private lists are omitted from the hosted subscriber email preferences/unsubscribe page and cannot be subscribed to or unsubscribed from individually there. Public lists expose their name and description on that page. List privacy does not override a subscriber's global unsubscribe.
          example: false
        createdAt:
          type: string
          format: date-time
        subscriberCount:
          type: integer
          description: Current list members of any status. Memberships with unsubscribedAt set are excluded.
        activeSubscriberCount:
          type: integer
          description: Current list members with status=active. May include phone-only contacts without an email address.

    TagDefinition:
      type: object
      properties:
        id:
          type: string
          example: "tag_abc123"
        name:
          type: string
          example: "premium"
        color:
          type: string
          nullable: true
          example: "#f97316"
        isSystem:
          type: boolean
          example: false

    EmailPreset:
      type: string
      enum: [branded, minimal]
      nullable: true
      description: >-
        Per-email Style > Format, derived from the stored blocks. `branded`
        renders the company logo and the full footer; `minimal` renders no logo
        and the simple footer. Null when the entire email is a single raw HTML
        block, which has no Sequenzy format. Sequence steps, campaigns,
        transactional templates, and saved templates all report this field, so
        chrome can be compared across emails without rendering them.
      example: "branded"

    CampaignSummary:
      type: object
      properties:
        id:
          type: string
          example: "camp_abc123"
        name:
          type: string
          example: "April Launch"
        type:
          type: string
          enum: [email, sms]
          description: >-
            Delivery channel. SMS campaigns are created and managed from the
            dashboard: they carry no email, so `subject` and `blocks` come back
            empty, and the email-only endpoints (update, schedule, pause,
            resume, duplicate, resend to non-openers) reject them. Campaign
            stats and campaign events reject them too, since both read email
            engagement.
          example: "email"
        subject:
          type: string
          example: "A quick update"
        emailId:
          type: string
          nullable: true
          description: >-
            The linked email body. The same record is returned by the templates
            endpoints and can be passed as `templateId` when creating campaigns
            to reuse the design. Null for SMS campaigns.
          example: "email_abc123"
        emailPreset:
          allOf:
            - $ref: "#/components/schemas/EmailPreset"
          description: >-
            Style > Format of the linked email. Null for SMS campaigns and for
            an email stored as a single raw HTML block.
        status:
          type: string
          enum:
            [
              draft,
              scheduled,
              waiting_approval,
              rejected,
              sending,
              paused,
              sent,
              cancelled,
            ]
          description: >-
            Campaign status. `waiting_approval` means the campaign is held for
            safety review and is not sending; this is most common on new
            accounts and recently registered sending domains. See
            https://docs.sequenzy.com/concepts/campaigns#safety-review
          example: "draft"
        rejectionComment:
          type: string
          nullable: true
          description: Reviewer feedback when the campaign status is rejected. Stays null while a campaign is still in waiting_approval.
        trackingCode:
          type: string
          nullable: true
          description: Campaign tracking code available to UTM templates as `{{campaign.trackingCode}}`.
        labels:
          type: array
          description: Label names assigned to this campaign.
          items:
            type: string
          example: ["edm", "api"]
        scheduledAt:
          type: string
          format: date-time
          nullable: true
        sentAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            For native sends, when the send finished. It is stamped after the
            last recipient is handed off, so for a spread or optimal-time send
            this is the end of the delivery window rather than when sending
            started. Imported campaigns retain the source provider's timestamp.
        createdAt:
          type: string
          format: date-time

    ResolvedAudienceEntity:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
          nullable: true
          description: Null when the referenced list or segment no longer exists.
        missing:
          type: boolean
          description: True when the ID points at a deleted or cross-company record.

    ResolvedAudienceRule:
      type: object
      properties:
        type:
          type: string
          enum: [all, lists, segments, filtered]
        lists:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceEntity"
        segments:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceEntity"
        filters:
          type: array
          items:
            type: object
            additionalProperties: true

    ResolvedCampaignAudience:
      type: object
      properties:
        type:
          type: string
          enum: [unset, all, lists, segment, filtered, rules]
        summary:
          type: string
          description: Plain-language description of who the campaign reaches.
        isUnset:
          type: boolean
          description: True when targeting has never been set, so scheduling sends to every active subscriber.
        lists:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceEntity"
        segments:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceEntity"
        filters:
          type: array
          items:
            type: object
            additionalProperties: true
        filterJoinOperator:
          type: string
          nullable: true
          enum: [and, or]
        include:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceRule"
        exclude:
          type: array
          items:
            $ref: "#/components/schemas/ResolvedAudienceRule"
        excludedSubscriberIds:
          type: array
          items:
            type: string
        includedSubscriberIds:
          type: array
          items:
            type: string
        excludedCampaignOpenerIds:
          type: array
          items:
            type: string

    IntegrationSummary:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
          example: shopify
        name:
          type: string
          description: Display name for the provider, falling back to the raw provider id.
          example: Shopify
        category:
          type: string
          nullable: true
          description: Provider category, or null for a provider with no catalog entry.
          enum:
            [
              payments,
              ecommerce,
              auth,
              analytics,
              ads,
              affiliate,
              cms,
              developer,
            ]
        providerAccountId:
          type: string
          description: Provider-side account identifier, such as a Shopify shop domain or Stripe acct_ ID.
        isActive:
          type: boolean
        syncEnabled:
          type: boolean
        syncStatus:
          type: string
          nullable: true
        lastSyncAt:
          type: string
          format: date-time
          nullable: true
        lastSyncError:
          type: string
          nullable: true
        totalCustomersSynced:
          type: integer
        totalEventsSynced:
          type: integer
        connectedAt:
          type: string
          format: date-time
        disconnectedAt:
          type: string
          format: date-time
          nullable: true
        details:
          type: object
          additionalProperties: true
          description: Allowlisted non-secret metadata. Never contains credentials.
        lastSyncSkipped:
          nullable: true
          description: Records the last sync could not import normally. Null when the sync was clean or the provider does not report skips.
          allOf:
            - $ref: "#/components/schemas/IntegrationSyncSkipSummary"

    EventSchema:
      type: object
      description: The published payload of one event. Listing mode returns the summary fields only; asking for a single eventName adds providers, mergeTagPrefix, and notes.
      properties:
        eventName:
          type: string
          description: Normalized event name - the name to trigger and to configure triggers on.
          example: ecommerce.order_placed
        requestedEventName:
          type: string
          description: The name as asked for, before alias normalization.
        documented:
          type: boolean
          description: Whether a reference payload is published. False never means the event name is invalid - custom events carry exactly the properties you send and are never rejected.
        label:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        documentedProviders:
          type: array
          description: Providers with a reference payload for this event.
          items:
            type: string
            enum: [shopify, woocommerce, manual, api, stripe]
        providers:
          type: array
          description: Single-event mode only. One entry per provider.
          items:
            type: object
            properties:
              provider:
                type: string
                enum: [shopify, woocommerce, manual, api, stripe]
              examplePayload:
                type: object
                description: A representative sample payload, not a contract.
                additionalProperties: true
              properties:
                type: array
                items:
                  type: object
                  properties:
                    path:
                      type: string
                      description: Dot path into the payload. Object array elements use [], e.g. lineItems[].priceCents.
                    type:
                      type: string
                      description: Human-readable type, e.g. string, string | null, object[].
                    description:
                      type: string
                      description: Present only where the sample value alone is ambiguous.
                    mergeTag:
                      type: string
                      description: Merge tag that resolves this path. Scalar paths only.
        mergeTagPrefix:
          type: string
          example: "event."
        notes:
          type: array
          items:
            type: string

    IntegrationProviderCapability:
      type: object
      description: What an integration provider does, independent of whether it is connected.
      properties:
        provider:
          type: string
          example: stripe
        name:
          type: string
          example: Stripe
        category:
          type: string
          enum:
            [
              payments,
              ecommerce,
              auth,
              analytics,
              ads,
              affiliate,
              cms,
              developer,
            ]
        availability:
          type: string
          enum: [available, beta, coming_soon]
          description: A coming_soon provider appears in the dashboard picker but has no webhook handler yet, so it emits nothing.
        summary:
          type: string
        connectMethod:
          type: string
          enum: [oauth, api_key, app_install, plugin, webhook]
        syncs:
          type: array
          description: What the integration keeps in sync. Empty when it only pushes events as they happen.
          items:
            type: string
        emits:
          type: array
          description: Every event the provider can produce. Empty when it never triggers automations.
          items:
            type: object
            properties:
              event:
                type: string
                description: Event name a sequence triggers on.
              when:
                type: string
                description: The real-world moment that produces this event.
        writesAttributes:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              description:
                type: string
        actions:
          type: array
          items:
            type: string
            enum:
              [
                connect,
                enable_sync,
                disable_sync,
                sync_now,
                sync_products,
                set_list_targeting,
                activate_pixel,
              ]
        connectFields:
          type: array
          description: Fields POST /integrations/connect accepts for this provider. Present exactly when actions includes connect.
          items:
            type: object
            properties:
              key:
                type: string
                enum:
                  [
                    apiKey,
                    webhookSecret,
                    providerAccountId,
                    settings,
                    historyImport,
                  ]
              required:
                type: boolean
              secret:
                type: boolean
                description: True when the value is a credential and must be handled as a secret.
              description:
                type: string
        notes:
          type: array
          description: Caveats worth knowing before building on the provider.
          items:
            type: string

    IntegrationEventWiring:
      type: object
      description: One emitted event crossed with the company's sync rules and sequences.
      properties:
        event:
          type: string
        when:
          type: string
        addsTags:
          type: array
          items:
            type: string
        removesTags:
          type: array
          items:
            type: string
        rules:
          type: array
          description: Every matching sync rule, kept separate so conditional effects remain accurate.
          items:
            type: object
            properties:
              addsTags:
                type: array
                items:
                  type: string
              removesTags:
                type: array
                items:
                  type: string
              conditions:
                type: object
                nullable: true
                additionalProperties: true
        observedByAccount:
          type: boolean
          description: Whether the account has received this event name from any source. Not integration-specific.
        accountLastSeenAt:
          type: string
          format: date-time
          nullable: true
          description: When the account last received the event name from any source.
        ruleSource:
          type: string
          enum: [custom, default, none]
          description: Where the matching sync rule came from. "none" means no rule touches this event, so it changes no tags.
        listeners:
          type: array
          description: Sequences that trigger on this event.
          items:
            type: object
            properties:
              sequenceId:
                type: string
              name:
                type: string
              effectiveStatus:
                type: string
              acceptsNewEnrollments:
                type: boolean
              effectiveStatusSummary:
                type: string

    IntegrationActivityEntry:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
        integrationId:
          type: string
          nullable: true
        source:
          type: string
        action:
          type: string
        status:
          type: string
          enum: [received, queued, processed, skipped, failed]
        eventType:
          type: string
          nullable: true
        providerEventId:
          type: string
          nullable: true
        jobId:
          type: string
          nullable: true
        subscriberId:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
          description: The contact the event was matched to, or null when none could be resolved.
        externalId:
          type: string
          nullable: true
        message:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
        requestPayload:
          type: object
          nullable: true
          additionalProperties: true
          description: Sanitized provider request metadata captured for diagnosis.
        responsePayload:
          type: object
          nullable: true
          additionalProperties: true
          description: Sanitized processing result metadata captured for diagnosis.
        receivedAt:
          type: string
          format: date-time
          nullable: true
        processedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time

    IntegrationDetail:
      type: object
      properties:
        success:
          type: boolean
          example: true
        integration:
          allOf:
            - $ref: "#/components/schemas/IntegrationSummary"
            - type: object
              properties:
                name:
                  type: string
                  description: Provider display name.
                category:
                  type: string
                  nullable: true
        capabilities:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/IntegrationProviderCapability"
        events:
          type: array
          items:
            $ref: "#/components/schemas/IntegrationEventWiring"
        unusedEvents:
          type: array
          description: Events the provider emits that no sequence triggers on.
          items:
            type: string
        accountNeverReceivedEvents:
          type: array
          description: Provider event names the account has never received from any source. Not integration-specific.
          items:
            type: string
        activity:
          type: object
          properties:
            windowHours:
              type: integer
              example: 24
            total:
              type: integer
            processed:
              type: integer
            failed:
              type: integer
            skipped:
              type: integer
            stalled:
              type: integer
              description: Events queued more than 15 minutes without completing.
            lastActivityAt:
              type: string
              format: date-time
              nullable: true
            recentFailures:
              type: array
              items:
                type: object
                properties:
                  action:
                    type: string
                  eventType:
                    type: string
                    nullable: true
                  email:
                    type: string
                    nullable: true
                  error:
                    type: string
                    nullable: true
                  createdAt:
                    type: string
                    format: date-time
        ingestion:
          type: object
          description: >-
            What this integration does to the contact list: whether bulk
            backfills run, and which lists the contacts created by the
            provider's live webhook join. Neither setting stops that webhook
            creating contacts.
          properties:
            bulkSyncEnabled:
              type: boolean
              description: Bulk imports and backfills. Same value as integration.syncEnabled.
            supportsListTargeting:
              type: boolean
            listTargeting:
              type: string
              nullable: true
              enum: [company_default, none, specific]
              description: Where contacts created by this integration land. Null for providers that ignore per-integration list targeting.
            listIds:
              type: array
              nullable: true
              description: Configured target lists. Null means new contacts follow the workspace default lists.
              items:
                type: string
            lists:
              type: array
              description: Names for the configured lists.
              items:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
            missingListIds:
              type: array
              description: Configured IDs whose list no longer exists. Deleting a list does not scrub it from integration settings and ingestion silently skips it, so these are targets nothing actually joins.
              items:
                type: string
            summary:
              type: string
              description: One sentence naming where this integration's new contacts land.
        pixel:
          type: object
          nullable: true
          description: >-
            Shopify only: live storefront tracking pixel state, read from the
            store on every call. Null for providers without a pixel. Same shape
            as the pixel endpoint, plus healthy and dependentEvents.
          properties:
            installed:
              type: boolean
            id:
              type: string
              nullable: true
            endpoint:
              type: string
              nullable: true
            endpointCurrent:
              type: boolean
            configurationCurrent:
              type: boolean
            healthy:
              type: boolean
            error:
              type: string
              nullable: true
            dependentEvents:
              type: array
              items:
                type: string
        recommendations:
          type: array
          items:
            type: object
            properties:
              code:
                type: string
              severity:
                type: string
                enum: [error, warning, info]
              message:
                type: string
              action:
                type: string
        availableActions:
          type: array
          description: Actions callable right now given the integration's current state.
          items:
            type: string

    IntegrationSyncSkipSummary:
      type: object
      description: >-
        Summary of records a store import could not bring in normally. A store
        with real order history often carries a few addresses on domains that
        have since been shut down; those are reported here instead of failing
        the whole import.
      properties:
        total:
          type: integer
          description: Every affected record, including any beyond the stored sample.
        suppressed:
          type: integer
          description: Imported, but the address cannot receive email, so the profile is stored as bounced and sends stay suppressed. Order history still attaches to it.
        skipped:
          type: integer
          description: Not imported at all.
        truncated:
          type: boolean
          description: True when more records were affected than `records` holds.
        records:
          type: array
          description: Sample of the affected records, up to 50.
          items:
            type: object
            properties:
              email:
                type: string
                nullable: true
              outcome:
                type: string
                enum: [suppressed, skipped]
              reasonCode:
                type: string
                example: invalid_email_domain
              reason:
                type: string
                example: 'Invalid domain for "buyer@example.com": Domain cannot receive email (no MX records)'

    SenderProfileSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        domainId:
          type: string
        domain:
          type: string
          nullable: true
        domainStatus:
          type: string
          nullable: true
          description: Aggregate verification status of the sending domain.
        canSend:
          type: boolean
          description: True when the domain is fully ready to send (DNS verified and activation finished).
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time

    ReplyProfileSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time

    SendingStatus:
      type: object
      description: Company-level sending state, the sender-health measurements behind it, and the remediation path. Enforcement is not time-windowed, so metricsWindow.expiresAt is always null.
      properties:
        success:
          type: boolean
          example: true
        status:
          type: string
          enum: [active, paused, suspended]
          description: Anything other than active blocks every send for this workspace, including test sends.
        pauseReason:
          type: string
          nullable: true
          description: Enforcement message including the measured rate, the threshold it crossed, and the volume it was measured over.
          example: Bounce rate (9.910%) exceeded threshold (5%) for 111 total emails
        pauseReasonKind:
          type: string
          nullable: true
          enum:
            [
              high_hard_bounce_rate,
              high_soft_bounce_rate,
              high_complaint_rate,
              phishing_guard,
              manual,
              other,
            ]
          description: Only high_hard_bounce_rate can be cleared through the resume endpoint.
        pausedAt:
          type: string
          format: date-time
          nullable: true
        selfResume:
          type: object
          properties:
            canSelfResume:
              type: boolean
              description: Whether POST /sending-status/resume will succeed right now.
            supported:
              type: boolean
              description: Whether this pause reason is self-resumable at all.
            allowedByAdmin:
              type: boolean
            ownerIsTrusted:
              type: boolean
            aiReviewStatus:
              type: string
              enum: [not_required, pending, approved, flagged, failed]
              description: State of the automated sender-health review opened when the pause was created.
            aiReviewReason:
              type: string
              nullable: true
            aiReviewStartedAt:
              type: string
              format: date-time
              nullable: true
            aiReviewedAt:
              type: string
              format: date-time
              nullable: true
            unavailableReason:
              type: string
              nullable: true
              enum:
                [
                  unsupported_reason,
                  waiting_for_review,
                  blocked_by_ai,
                  review_failed,
                  blocked_by_admin,
                ]
              description: The gate blocking resume. Null when resume is available.
        senderHealth:
          type: object
          nullable: true
          description: Null when the workspace has no metrics record yet or sender-health analytics are temporarily unavailable. Pause state and remediation remain available during an analytics outage.
          properties:
            enforcementMode:
              type: string
              enum: [enforce, monitor_only]
            scopedSent:
              type: integer
              description: Backward-compatible alias for bounceScopedSent.
            bounceScopedSent:
              type: integer
              description: Non-test sends counted from the bounce reset watermark - the denominator for hard- and soft-bounce rates.
            complaintScopedSent:
              type: integer
              description: Non-test sends counted from the complaint reset watermark - the denominator for complaint rates.
            hardBounce:
              $ref: "#/components/schemas/SenderHealthMetric"
            softBounce:
              $ref: "#/components/schemas/SenderHealthMetric"
            complaint:
              $ref: "#/components/schemas/SenderHealthMetric"
        metricsWindow:
          type: object
          properties:
            kind:
              type: string
              enum: [all_time_since_reset]
            expiresAt:
              type: string
              nullable: true
              description: Always null. Enforcement totals are all-time from the watermark, so a paused rate never expires on its own.
            bounceResetAt:
              type: string
              format: date-time
              nullable: true
            complaintResetAt:
              type: string
              format: date-time
              nullable: true
            description:
              type: string
        remediation:
          type: object
          properties:
            steps:
              type: array
              description: Ordered next steps for the current state.
              items:
                type: string
            supportEmail:
              type: string
            docsUrl:
              type: string
            dashboardPath:
              type: string
              nullable: true
            dashboardUrl:
              type: string
              nullable: true

    SenderHealthMetric:
      type: object
      properties:
        count:
          type: integer
        rate:
          type: number
          description: Percentage of the applicable bounceScopedSent or complaintScopedSent denominator.
        warnThreshold:
          $ref: "#/components/schemas/SenderHealthThresholdBound"
        pauseThreshold:
          $ref: "#/components/schemas/SenderHealthThresholdBound"

    SenderHealthThresholdBound:
      type: object
      description: Threshold that applies at this send volume. Either bound may be null when the tier does not use it.
      properties:
        rate:
          type: number
          nullable: true
        count:
          type: integer
          nullable: true

    TrackingSettings:
      type: object
      properties:
        success:
          type: boolean
          example: true
        tracking:
          type: object
          properties:
            openTrackingEnabled:
              type: boolean
            clickTrackingEnabled:
              type: boolean
            strictBotFilteringEnabled:
              type: boolean
              description: Opt-in aggressive bot detection (strict user-agent patterns, datacenter IPs, cross-send IP sweeps). Off by default; enabling it can lower reported open and click rates.
            unsubscribeTrackingEnabled:
              type: boolean
            defaultAttributionWindowHours:
              type: integer
        consent:
          type: object
          description: Signup consent settings, as opposed to engagement measurement.
          properties:
            doubleOptInEnabled:
              type: boolean
              description: Whether new contacts must confirm by email before they become subscribed. When on, contacts added by forms, the API, and integrations start pending and are never sent marketing email until they confirm.
            doubleOptInEmailId:
              type: string
              nullable: true
              description: Confirmation email sent to pending contacts. Null when double opt-in has never been enabled; enabling it provisions one automatically.
        autoUtm:
          type: object
          properties:
            enabled:
              type: boolean
            settings:
              type: object
              additionalProperties: true
        trackingDomain:
          type: object
          nullable: true
          description: Null when click links use the shared Sequenzy tracking domain.
          properties:
            domain:
              type: string
            status:
              type: string
            sslStatus:
              type: string
              nullable: true
            verifiedAt:
              type: string
              format: date-time
              nullable: true
            lastCheckedAt:
              type: string
              format: date-time
              nullable: true
            error:
              type: string
              nullable: true
        replyTracking:
          type: object
          properties:
            inboundEmailEnabled:
              type: boolean
            inboundReplyDomainMode:
              type: string
            forwardReplies:
              type: boolean

    CampaignDetail:
      allOf:
        - $ref: "#/components/schemas/CampaignSummary"
        - type: object
          properties:
            preheader:
              type: string
              example: "Latest product news"
            preheaderText:
              type: string
              description: Compatibility alias for preheader.
              example: "Latest product news"
            blocks:
              type: array
              items:
                $ref: "#/components/schemas/EmailBlock"
            campaignData:
              type: object
              nullable: true
              additionalProperties: true
            computedLists:
              type: array
              items:
                type: object
                additionalProperties: true
            targetLists:
              type: object
              nullable: true
              additionalProperties: true
              description: Saved campaign audience, or null when targeting is still unset and scheduling will fall back to all active subscribers.
            senderProfileId:
              type: string
            fromName:
              type: string
              nullable: true
            fromEmail:
              type: string
              format: email
              nullable: true
            replyProfileId:
              type: string
              nullable: true
              example: "reply_abc123"
            replyToName:
              type: string
              nullable: true
              example: "Support"
            replyToEmail:
              type: string
              format: email
              nullable: true
              example: "support@example.com"
            ccEmails:
              type: array
              nullable: true
              description: Addresses CC'd on every recipient's email for this campaign.
              items:
                type: string
                format: email
            bccEmails:
              type: array
              nullable: true
              description: Addresses BCC'd on every recipient's email for this campaign.
              items:
                type: string
                format: email
            spreadOverHours:
              type: integer
              nullable: true
              description: >-
                Hours the send was (or will be) spread over. Null means there
                is no fixed spread; inspect sendTimeOptimization to distinguish
                an optimal-time send from a campaign with no recorded pacing.
                Imported campaigns may not include source-provider pacing data.
              example: 6
            sendTimeOptimization:
              type: boolean
              description: >-
                Whether each recipient is sent at their best predicted open
                time. Always false when spreadOverHours is set: setting a
                spread clears send time optimization.
            sendTimeWindowHours:
              type: integer
              nullable: true
              description: >-
                Window send time optimization may deliver within, in hours.
                Defaults to 12 and only applies when sendTimeOptimization is
                true.
              example: 12
            sendInRecipientTimezone:
              type: boolean
              description: Whether the campaign is delivered at the scheduled wall-clock time in each recipient's timezone.
            scheduledTimezone:
              type: string
              nullable: true
              maxLength: 64
              description: Stored IANA timezone that anchors the campaign's scheduled wall-clock time. Use sendInRecipientTimezone to determine whether recipient-timezone delivery is enabled; this value may remain set when that mode is disabled.
              example: "America/New_York"
            shareUrl:
              type: string
              nullable: true
              description: >-
                Public anonymized view-in-browser link, or null until one is
                minted via POST /campaigns/{campaignId}/share-link.
              example: "https://sequenzy.com/view/campaign/8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s"

    LandingPageContent:
      type: object
      description: Landing page builder JSON.
      additionalProperties: true
      properties:
        version:
          type: integer
          example: 2
        template:
          type: string
          example: "waitlist"
        seo:
          type: object
          additionalProperties: true
          description: Search and browser metadata for the published page.
          properties:
            title:
              type: string
              maxLength: 70
            description:
              type: string
              maxLength: 160
            faviconUrl:
              type: string
              maxLength: 1000
              description: >-
                Browser tab icon for the published page. Leave empty to fall
                back to the company logo.
              example: "https://acme.com/favicon.png"
            hideFromSearchEngines:
              type: boolean
              default: false
              description: >-
                Adds a noindex, nofollow tag so crawlers keep the page out of
                search results. The page stays reachable by direct link.
        theme:
          type: object
          additionalProperties: true
          description: Page-wide design settings.
          properties:
            sectionAnimation:
              type: string
              enum: [none, fade, slide-up, zoom-in]
              default: none
              description: >-
                Scroll reveal each section plays as it enters the viewport on
                the published page. Skipped for visitors who prefer reduced
                motion.
            sectionAnimationSpeed:
              type: string
              enum: [slow, normal, fast]
              default: normal
              description: How quickly the scroll reveal settles.
        blocks:
          type: array
          description: >-
            Page blocks. Each block has a `slot`, rendered in order: `top`
            (full-width band above the hero), `hero`, `form` (the card beside
            the hero), `body`, `footer`. A `video` block embeds a pasted
            YouTube URL from its `url`; other providers and direct video files
            are not supported. It accepts an optional `aspectRatio` of 16:9,
            4:3, 1:1, or 9:16. Button `url` and pricing `buttonUrl`
            accept an https URL or an in-page anchor: `#form` scrolls to the
            page's form block, `#section-<sectionId>` and `#block-<blockId>`
            scroll to any section or block, and `#top` returns to the top.
            Anchor CTAs open in the same tab.
          items:
            type: object
            additionalProperties: true
      required:
        - version
        - blocks

    LandingPageSummary:
      type: object
      properties:
        id:
          type: string
          example: "lp_abc123"
        companyId:
          type: string
          example: "comp_abc123"
        name:
          type: string
          example: "Product Waitlist"
        slug:
          type: string
          example: "product-waitlist"
        status:
          type: string
          enum: [draft, published]
          example: "published"
        content:
          $ref: "#/components/schemas/LandingPageContent"
        viewCount:
          type: integer
          example: 1240
        conversionCount:
          type: integer
          example: 186
        publishedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          description: Dashboard edit URL.
          example: "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages/lp_abc123"
        appPublicUrl:
          type: string
          format: uri
          nullable: true
          description: Sequenzy-hosted public URL when published.
          example: "https://sequenzy.com/lp/comp_abc123/product-waitlist"
        customDomain:
          type: string
          nullable: true
          description: Preferred verified custom hostname for this page.
          example: "offer.example.com"
        customDomainScope:
          type: string
          enum: [page, workspace]
          nullable: true
          description: Whether the preferred hostname is dedicated to this page or inherited from the workspace.
        publicUrl:
          type: string
          format: uri
          nullable: true
          description: Custom-domain public URL when a verified custom domain is connected, otherwise the Sequenzy-hosted public URL.
          example: "https://pages.example.com/product-waitlist"

    LandingPageDomain:
      type: object
      properties:
        domain:
          type: string
          nullable: true
          example: "pages.example.com"
        domainScope:
          type: string
          enum: [page, workspace]
          nullable: true
          description: Domain assignment scope.
        landingPageId:
          type: string
          nullable: true
          description: Assigned landing page ID for a dedicated page domain.
        domainStatus:
          type: string
          example: "pending"
        fallbackDomain:
          type: string
          nullable: true
          description: Verified or pending workspace domain retained as a fallback for a page-scoped domain.
        fallbackDomainStatus:
          type: string
          default: not_started
          description: Workspace fallback-domain status; `not_started` when no fallback domain is configured.
        verifiedAt:
          type: string
          format: date-time
          nullable: true
        dnsRecordAddedAt:
          type: string
          format: date-time
          nullable: true
        sslStatus:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
        verificationRecords:
          type: array
          items:
            type: object
            additionalProperties: true
        lastCheckedAt:
          type: string
          format: date-time
          nullable: true
        cnameTarget:
          type: string
          example: "pages.sequenzydns.com"
        dnsRecordType:
          type: string
          enum: [A, CNAME]
          description: DNS record type to add - CNAME for subdomains, A for root domains.
        dnsRecordName:
          type: string
          nullable: true
          description: DNS record name (the connected hostname); null when no domain is connected.
          example: "pages.example.com"
        dnsRecordValue:
          type: string
          description: DNS record value - the CNAME target for subdomains or the A record IP (76.76.21.21) for root domains.
          example: "pages.sequenzydns.com"

    TemplateSummary:
      type: object
      properties:
        id:
          type: string
          example: "email_abc123"
        name:
          type: string
          example: "[Template] Welcome"
        subject:
          type: string
          example: "Welcome!"
        previewText:
          type: string
          nullable: true
        emailPreset:
          $ref: "#/components/schemas/EmailPreset"
        labels:
          type: array
          description: Label names assigned to this template.
          items:
            type: string
          example: ["edm", "template"]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        localizations:
          type: array
          items:
            type: object
            additionalProperties: true

    TemplateDetail:
      allOf:
        - $ref: "#/components/schemas/TemplateSummary"
        - type: object
          properties:
            companyId:
              type: string
              example: "company_abc123"
            blocks:
              type: array
              items:
                $ref: "#/components/schemas/EmailBlock"
            fontFamily:
              type: string
              nullable: true
            shareUrl:
              type: string
              nullable: true
              description: |
                Public anonymized view-in-browser URL. Null until a link is
                minted via POST /templates/{templateId}/share-link.
            emailLocalizationConfig:
              type: object
              additionalProperties: true

    TemplateLocalization:
      type: object
      properties:
        locale:
          type: string
          example: "es"
        status:
          type: string
          enum: [synced, stale, syncing, failed]
        subject:
          type: string
          example: "Bienvenido"
        previewText:
          type: string
          nullable: true
        blocks:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlock"
        sourceHash:
          type: string
          nullable: true
        lastError:
          type: string
          nullable: true
        syncedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ABTestVariant:
      type: object
      properties:
        id:
          type: string
          example: "var_a"
        variantId:
          type: string
          example: "var_a"
        abTestId:
          type: string
          example: "ab_abc123"
        label:
          type: string
          example: "A"
        variantLabel:
          type: string
          example: "A"
        emailId:
          type: string
          example: "email_abc123"
        subject:
          type: string
          example: "Welcome"
        previewText:
          type: string
          nullable: true
        blocks:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlock"
        testSends:
          type: integer
        testOpens:
          type: integer
        testClicks:
          type: integer
        isWinner:
          type: boolean
        localizations:
          type: array
          items:
            type: object
            additionalProperties: true
        createdAt:
          type: string
          format: date-time

    ABTest:
      type: object
      properties:
        id:
          type: string
          example: "ab_abc123"
        companyId:
          type: string
          example: "company_abc123"
        kind:
          type: string
          enum: [campaign, sequence]
          description: Identifies which settings model applies to this test.
        campaignId:
          type: string
          nullable: true
          example: "camp_abc123"
        automationNodeId:
          type: string
          nullable: true
          example: "node_abc123"
        name:
          type: string
          nullable: true
          example: "Subject test"
        status:
          type: string
          example: "draft"
        testPercentage:
          type: integer
          description: Campaign test audience percentage. Sequence tests retain the legacy internal sentinel value 100; use settings instead.
        testDurationMinutes:
          type: integer
          description: Campaign test duration. Sequence tests retain the legacy internal sentinel value 0; use settings instead.
        winnerCriteria:
          type: string
          example: "open_rate"
        testType:
          type: string
          enum: [subject, content]
          description: Effective sequence variant strategy. Present for sequence tests.
        winnerThreshold:
          type: integer
          description: Effective sequence recipient threshold. Present for sequence tests.
        settings:
          type: object
          additionalProperties: true
          description: Effective settings for this test kind. Campaign tests return testPercentage, testDurationMinutes, and winnerCriteria; sequence tests return testType, winnerThreshold, and winnerCriteria.
        winningVariantId:
          type: string
          nullable: true
        winnerSelectedAt:
          type: string
          format: date-time
          nullable: true
        testStartedAt:
          type: string
          format: date-time
          nullable: true
        testEndsAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        variants:
          type: array
          items:
            $ref: "#/components/schemas/ABTestVariant"

    Subscriber:
      type: object
      properties:
        id:
          type: string
          example: "sub_abc123"
        email:
          type: string
          format: email
          nullable: true
          description: Null for phone-only (SMS) contacts, which are identified by their phone number instead.
          example: "user@example.com"
        externalId:
          type: string
          nullable: true
          description: Customer-owned app/customer/user ID for this subscriber
          example: "user_123"
        firstName:
          type: string
          nullable: true
          example: "John"
        lastName:
          type: string
          nullable: true
          example: "Doe"
        timezone:
          type: string
          nullable: true
          description: IANA timezone identifier used for recipient-local campaign delivery. Null when unknown.
          example: "America/New_York"
        phone:
          type: string
          nullable: true
          description: Phone number in E.164 format
          example: "+15551234567"
        smsStatus:
          type: string
          enum: [not_subscribed, pending, subscribed, unsubscribed]
          description: SMS marketing consent status, independent of the email status
          example: "subscribed"
        status:
          type: string
          enum: [active, unsubscribed, bounced]
          example: "active"
        unsubscribedAt:
          type: string
          format: date-time
          nullable: true
          description: When the contact opted out, derived from the list memberships the opt-out deactivated. Use this rather than `updatedAt` to date an opt-out - any later tag or attribute write moves `updatedAt`. Null unless the contact is currently unsubscribed, so leaving a single list does not set it, and null for contacts imported as already unsubscribed, where no date exists.
          example: "2026-07-15T10:00:00.000Z"
        emailProvider:
          type: string
          nullable: true
          example: "gmail"
        tags:
          type: array
          items:
            type: string
          example: ["premium", "newsletter"]
        customAttributes:
          type: object
          additionalProperties: true
          example: { "plan": "pro", "signupSource": "website" }
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SubscriberImportRecord:
      type: object
      description: Every record must include an email or a phone. Records with only a phone import as phone-only (SMS) contacts.
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
          description: Optional when the record has a phone.
        externalId:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        timezone:
          type: string
          maxLength: 64
          description: IANA timezone identifier (e.g. America/New_York) used for recipient-local campaign delivery. Records with an invalid value import without it.
        phone:
          type: string
          maxLength: 64
          description: Phone number. National-format values use the batch defaultPhoneCountry. Required when the record has no email.
        status:
          type: string
          enum: [active, unsubscribed, bounced]
        tags:
          type: array
          items:
            type: string
        customAttributes:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
              - type: array
                items:
                  oneOf:
                    - type: string
                    - type: number
                    - type: boolean
              - type: "null"
        createdAt:
          type: string
          format: date-time
          description: Original signup date on the source platform. Preserves the contact's real history so date-relative segments are correct right after the import. An existing contact's date only ever moves earlier. An unusable value rejects the whole request with a 400 naming the row.
          example: "2019-03-14T09:30:00Z"

    SubscriberImport:
      type: object
      properties:
        id:
          type: string
        batchId:
          type: string
        fileName:
          type: string
        status:
          type: string
          enum: [running, completed]
        totalRows:
          type: integer
        emailCount:
          type: integer
        duplicateRows:
          type: integer
        addedCount:
          type: integer
        updatedCount:
          type: integer
        skippedCount:
          type: integer
        failedCount:
          type: integer
        processedCount:
          type: integer
        totalChunks:
          type: integer
        completedChunks:
          type: integer
        failedChunks:
          type: integer
        skippedReasons:
          type: object
          description: Count per reason a row was skipped. The values sum to skippedCount.
          additionalProperties:
            type: integer
        failedReasons:
          type: object
          additionalProperties:
            type: integer
        failedChunkReasons:
          type: object
          additionalProperties:
            type: integer
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SubscriberNote:
      type: object
      properties:
        id:
          type: string
          example: "note_123"
        companyId:
          type: string
          example: "comp_abc123"
        subscriberId:
          type: string
          example: "sub_abc123"
        authorId:
          type: string
          example: "user_123"
        body:
          type: string
          example: "Asked about enterprise pricing"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        author:
          type: object
          nullable: true
          properties:
            id:
              type: string
              example: "user_123"
            name:
              type: string
              nullable: true
              example: "Ana"
            email:
              type: string
              nullable: true
              example: "ana@example.com"

    SubscriberListMembership:
      type: object
      properties:
        id:
          type: string
          example: "list_vip"
        name:
          type: string
          example: "VIP Customers"
        description:
          type: string
          nullable: true
        isPrivate:
          type: boolean
          nullable: true
          description: Whether the list is private. Private lists are omitted from the hosted subscriber email preferences/unsubscribe page and cannot be subscribed to or unsubscribed from individually there. List privacy does not override a subscriber's global unsubscribe.
        subscribedAt:
          type: string
          format: date-time
        unsubscribedAt:
          type: string
          format: date-time
          nullable: true

    SubscriberSequenceEnrollment:
      type: object
      properties:
        tokenId:
          type: string
          example: "token_123"
        sequenceId:
          type: string
          example: "seq_welcome"
        sequenceName:
          type: string
          example: "Welcome Flow"
        sequenceStatus:
          type: string
          example: "active"
        enrollmentStatus:
          type: string
          example: "active"
        currentNodeId:
          type: string
          example: "node_abc"
        currentNodeType:
          type: string
          nullable: true
          example: "action_email"
        currentNodeLabel:
          type: string
          nullable: true
          example: "Send welcome email"
        scheduledFor:
          type: string
          format: date-time
          nullable: true
        enteredAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SubscriberEmailStats:
      type: object
      properties:
        sent:
          type: integer
        delivered:
          type: integer
        opened:
          type: integer
        clicked:
          type: integer
        bounced:
          type: integer
        unsubscribed:
          type: integer
        complained:
          type: integer

    SubscriberActivityEvent:
      type: object
      properties:
        id:
          type: string
        eventType:
          type: string
          example: "custom"
          description: >
            Email event type (send, delivery, open, click, bounce, complaint,
            unsubscribe, delivery_delay), "custom" for subscriber events, or an
            SMS event type (sms_sent, sms_delivered, sms_failed, sms_clicked,
            sms_opted_out). New types may be added over time.
        eventTime:
          type: string
          format: date-time
        emailSendId:
          type: string
          nullable: true
        campaignId:
          type: string
          nullable: true
        clickedUrl:
          type: string
          nullable: true
        machine:
          type: boolean
          description: Whether this open/click event is classified as bot/scanner activity.
        engagementQuality:
          type: string
          nullable: true
          enum: [human, machine, asset]
          description: Engagement classification for open/click events.
        classificationReasons:
          type: array
          items:
            type: string
          description: Classification reason codes for open/click events.
        bounceType:
          type: string
          nullable: true
        smsSendId:
          type: string
          nullable: true
          description: SMS send this event belongs to. Present only on sms_* events.
        smsErrorCode:
          type: string
          nullable: true
          description: Carrier or provider error code. Present only on sms_failed events.
        smsSegments:
          type: integer
          nullable: true
          description: Number of SMS segments the message was split into. Present only on sms_* events.
        eventName:
          type: string
          nullable: true
          example: "saas.purchase"
        properties:
          type: object
          nullable: true
          additionalProperties: true

    FilterLeaf:
      type: object
      description: A single subscriber filter rule.
      properties:
        kind:
          type: string
          enum: [filter]
          description: Required when the filter is inside a v2 root group.
        id:
          type: string
          example: "filter-1"
        field:
          type: string
          enum:
            - status
            - phone
            - smsStatus
            - tag
            - email
            - emailProvider
            - added
            - firstName
            - lastName
            - list
            - attribute
            - event
            - segment
            - stripeProduct
            - stripeCurrentProduct
            - stripeTrialProduct
            - commerceProduct
            - commerceCollection
            - emailSent
            - emailDelivered
            - emailOpened
            - emailClicked
            - emailBounced
            - emailComplained
        operator:
          type: string
          description: "Valid operators depend on the field. status/segment: is, is_not. smsStatus: is, is_not (values: subscribed, unsubscribed, not_subscribed). phone: is_not_empty, is_empty (empty value). tag: contains, not_contains, is_empty, is_not_empty. email: contains, not_contains for domain or substring matching, is, is_not for an exact case-insensitive address. emailProvider/list: is, is_not, is_empty, is_not_empty. firstName/lastName: contains, not_contains, is_empty, is_not_empty. added: less_than, more_than. attribute: is, is_not, is_empty, is_not_empty, gte, lte, gt, lt, contains, not_contains. event and email engagement fields: is, is_not, at_least, less_than_count. emailBounced also supports is_temporary_bounce and is_permanent_bounce. stripeProduct: is, is_not, at_least, less_than_count. stripeCurrentProduct/stripeTrialProduct: is, is_not, gte, lte, gt, lt. commerceProduct/commerceCollection: is, is_not, at_least, less_than_count."
          enum:
            - is
            - is_not
            - is_empty
            - is_not_empty
            - contains
            - not_contains
            - less_than
            - more_than
            - is_temporary_bounce
            - is_permanent_bounce
            - at_least
            - less_than_count
            - gte
            - lte
            - gt
            - lt
        value:
          type: string
          description: Event filters use `eventName:30d` or `eventName:5:30d`. Segment filters use a segment ID. Email engagement fields use a rolling time window (`7d`, `30d`, `90d`, `180d`, `all`), a specific campaign via `campaign:<campaign_id>`, or `count:timeRange` (such as `10:30d` or `10:all`) with at_least/less_than_count. Stripe product filters use `prod_123` for bought/current/trialing checks, `prod_123:3` for payment thresholds, and product-scoped values such as `prod_123:is_canceled`, `prod_123:cancels_at:2026-05-26`, `prod_123:end_at:2026-05-26`, or `prod_123:start_at:7 days ago`. Commerce product filters use `provider:productId` (provider one of `shopify`, `woocommerce`, `api`), optionally with an order-count threshold (`shopify:42:2`); a bare product ID matches the ID on any provider. Commerce collection filters use a collection ID or handle (`skincare`), optionally provider-prefixed and/or with an order-count threshold (`shopify:skincare:2`), and match anyone whose orders contain any product currently in that collection.
      required:
        - id
        - field
        - operator
        - value

    FilterGroup:
      type: object
      description: A nested AND/OR filter group.
      properties:
        kind:
          type: string
          enum: [group]
        id:
          type: string
          example: "root"
        joinOperator:
          type: string
          enum: [and, or]
        children:
          type: array
          items:
            oneOf:
              - $ref: "#/components/schemas/FilterLeaf"
              - $ref: "#/components/schemas/FilterGroup"
            discriminator:
              propertyName: kind
      required:
        - kind
        - id
        - joinOperator
        - children

    Segment:
      type: object
      properties:
        id:
          type: string
          example: "seg_active_buyers"
        name:
          type: string
          example: "Active buyers"
        filters:
          type: array
          description: Legacy v1 flat filters, or flattened leaves for v2 responses.
          items:
            $ref: "#/components/schemas/FilterLeaf"
        filterJoinOperator:
          type: string
          enum: [and, or]
        format:
          type: string
          enum: [v1, v2]
        root:
          allOf:
            - $ref: "#/components/schemas/FilterGroup"
          nullable: true
        subscriberCount:
          type: integer
        activeSubscriberCount:
          type: integer

    AudienceSync:
      type: object
      description: A segment-to-Meta-custom-audience sync mapping.
      properties:
        id:
          type: string
          example: "sync_abc123"
        segmentId:
          type: string
          example: "seg_recent_buyers"
        segmentName:
          type: string
          nullable: true
          example: "Recent buyers"
        adAccountId:
          type: string
          example: "act_1234567890"
        audienceName:
          type: string
          example: "Sequenzy - Recent buyers"
        frequency:
          type: string
          enum: [hourly, daily, weekly]
        isActive:
          type: boolean
        syncStatus:
          type: string
          description: idle, syncing, error, or disconnected (the Meta connection was revoked; the sync is paused until you reconnect)
          example: "idle"
        lastSyncAt:
          type: string
          format: date-time
          nullable: true
        lastSyncError:
          type: string
          nullable: true
        lastSyncedCount:
          type: integer
          description: Subscribers uploaded in the most recent run.
        createdAt:
          type: string
          format: date-time

    AdAccount:
      type: object
      properties:
        id:
          type: string
          example: "act_1234567890"
        name:
          type: string
          example: "Acme Main Account"

    ProductDigitalDelivery:
      type: object
      nullable: true
      description: Distributable file delivered after a purchase of this product.
      properties:
        url:
          type: string
          example: "https://images.sequenzy.com/product-files/comp_123/9f2.../guide.pdf"
        source:
          type: string
          enum: [upload, url]
        fileName:
          type: string
          nullable: true
          example: "guide.pdf"
        fileSizeBytes:
          type: integer
          nullable: true
        mimeType:
          type: string
          nullable: true
          example: "application/pdf"
        updatedAt:
          type: string
          format: date-time
          nullable: true

    Product:
      type: object
      properties:
        id:
          type: string
          example: "prod_internal_1"
        provider:
          type: string
          enum: [api, stripe, shopify, woocommerce, manual]
        providerProductId:
          type: string
          nullable: true
          example: "prod_ABC123"
        title:
          type: string
          example: "The Indie Hacker Guide"
        description:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        price:
          type: integer
          nullable: true
          description: Price in cents
          example: 4900
        currency:
          type: string
          nullable: true
          example: "USD"
        url:
          type: string
          nullable: true
        inStock:
          type: boolean
          nullable: true
          description: For Stripe products, false means archived in Stripe.
        digitalDelivery:
          $ref: "#/components/schemas/ProductDigitalDelivery"
        createdAt:
          type: string
          format: date-time
          nullable: true
        updatedAt:
          type: string
          format: date-time
          nullable: true

    DetailedSubscriber:
      allOf:
        - $ref: "#/components/schemas/Subscriber"
        - type: object
          properties:
            lists:
              type: array
              items:
                $ref: "#/components/schemas/SubscriberListMembership"
            sequenceEnrollments:
              type: array
              items:
                $ref: "#/components/schemas/SubscriberSequenceEnrollment"
            notes:
              type: array
              items:
                $ref: "#/components/schemas/SubscriberNote"
            emailStats:
              allOf:
                - $ref: "#/components/schemas/SubscriberEmailStats"
              nullable: true
            activity:
              type: array
              items:
                $ref: "#/components/schemas/SubscriberActivityEvent"

    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 20
        total:
          type: integer
          example: 100
        totalPages:
          type: integer
          example: 5

    SubscriberListPagination:
      type: object
      description: Pagination for the subscriber list endpoint. `total` and `totalPages` are null on cursor requests because the count query is skipped.
      properties:
        page:
          type: integer
          example: 1
        limit:
          type: integer
          example: 1000
        total:
          type: integer
          nullable: true
          example: 8718
        totalPages:
          type: integer
          nullable: true
          example: 9
        nextCursor:
          type: string
          nullable: true
          description: Pass back as `cursor` to fetch the next page. Null when there are no further results.
          example: "eyJ2ZXJzaW9uIjoxLCJzb3J0IjoiY3JlYXRlZF9hdF9kZXNjX2lkX2Rlc2MifQ"
        hasMore:
          type: boolean
          example: true
        orderBy:
          type: string
          description: Sort key the cursor walks.
          example: "created_at_desc_id_desc"

    TransactionalEmail:
      type: object
      properties:
        id:
          type: string
          example: "tx_abc123"
        name:
          type: string
          example: "Welcome Email"
        slug:
          type: string
          example: "welcome-email"
        emailId:
          type: string
          example: "email_123"
        enabled:
          type: boolean
          example: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TransactionalEmailListItem:
      allOf:
        - $ref: "#/components/schemas/TransactionalEmail"
        - type: object
          properties:
            subject:
              type: string
              nullable: true
            emailPreset:
              $ref: "#/components/schemas/EmailPreset"
            stats:
              type: object
              properties:
                sends:
                  type: integer
                deliveries:
                  type: integer
                opens:
                  type: integer
                clicks:
                  type: integer
                bounces:
                  type: integer
                openRate:
                  type: number
                  format: float
                clickRate:
                  type: number
                  format: float

    TransactionalEmailDetails:
      allOf:
        - $ref: "#/components/schemas/TransactionalEmail"
        - type: object
          properties:
            variables:
              type: array
              items:
                type: string
              example: ["NAME", "COMPANY", "PRODUCT"]
            subject:
              type: string
              nullable: true
              example: "Welcome, {{NAME}}"
            previewText:
              type: string
              nullable: true
              example: "Your account is ready."
            emailPreset:
              $ref: "#/components/schemas/EmailPreset"
            blocks:
              type: array
              items:
                $ref: "#/components/schemas/EmailBlock"
            email:
              oneOf:
                - $ref: "#/components/schemas/Email"
                - type: "null"

    Attachment:
      type: object
      required:
        - filename
      properties:
        filename:
          type: string
          description: The filename for the attachment (including extension)
          example: "invoice.pdf"
        content:
          type: string
          description: Base64-encoded file content (mutually exclusive with path)
          example: "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC..."
        path:
          type: string
          format: uri
          description: URL to fetch the file from (mutually exclusive with content)
          example: "https://example.com/files/invoice.pdf"
        contentType:
          type: string
          description: MIME type of the attachment (optional, auto-detected from the filename if not provided)
          example: "application/pdf"
        contentId:
          type: string
          description: Content-ID that embeds the file as an inline image instead of attaching it. Reference it from the HTML body as `<img src="cid:VALUE">`; the message is then sent as multipart/related, which is the only embedded-image form Gmail renders. If nothing in the HTML references the value, the file is sent as a normal attachment.
          example: "ticket-qr"

    UrlAttachment:
      type: object
      description: URL-backed email attachment. The file is fetched from the URL at send time (max 10 attachments and 7MB total per email). For event-triggered sequences, path may be an event merge tag such as {{event.file_url}} that resolves to a public URL for each enrollment. Base64 content is not supported here.
      required:
        - filename
        - path
      properties:
        filename:
          type: string
          description: Filename shown in the recipient's email client (including extension). Event merge tags are supported.
          example: "guide.pdf"
        path:
          type: string
          description: Public HTTP(S) URL or an event-backed URL template such as {{event.file_url}}. The resolved URL is validated and fetched at send time.
          example: "https://example.com/files/guide.pdf"

    EmailBlockTypeReference:
      type: object
      description: >-
        Field reference for one email block type. Lists are their own block type
        rather than a text variant: a `text` block accepts only variant
        paragraph, lead, or html and never accepts `items`.
      properties:
        type:
          type: string
          example: "steps"
        creatable:
          type: boolean
          description: False for structural types the editor manages, which should not be hand-authored.
        reason:
          type: string
          description: Why a non-creatable type is excluded.
        required:
          type: array
          items:
            type: string
          example: ["items", "variant"]
        optional:
          type: array
          items:
            type: string
          example: ["id", "accentColor", "styles", "conditions"]
        fields:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlockFieldReference"
        example:
          type: object
          description: A minimal valid block of this type.
          additionalProperties: true
        notes:
          type: array
          items:
            type: string
    EmailBlockConditionFieldReference:
      type: object
      description: >-
        One field a block condition may filter on. A block's own `field` and
        `operator` entries are two flat enums that pool every field's operators
        together, and the schema narrows them against each other when it
        validates - so `{"field": "tag", "operator": "is"}` parses and is then
        rejected. This is that narrowing, spelled out.
      properties:
        field:
          type: string
          example: "tag"
        label:
          type: string
          example: "Tag"
        operators:
          type: array
          description: The only operators this field accepts.
          items:
            type: string
          example: ["contains", "not_contains", "is_empty", "is_not_empty"]
        values:
          type: array
          description: Closed value set, for the fields that have one.
          items:
            type: string
        valueFormat:
          type: string
          description: How the `value` string is shaped for this field.
        serverEvaluated:
          type: boolean
          description: >-
            Whether the field reads stored subscriber state rather than the
            merge data of the send.
        previewSupport:
          type: string
          enum:
            [any_contact, inline_tags_or_stored_subscriber, stored_subscriber]
          description: >-
            What a render needs before it can evaluate this field. any_contact -
            resolved from merge data. inline_tags_or_stored_subscriber - tag,
            which an inline contact can state for itself. stored_subscriber -
            needs subscriberId. A condition a render cannot evaluate renders as
            false and is listed in that render's unevaluatedConditions.
        example:
          type: object
          description: A valid condition using this field.
          additionalProperties: true
      required:
        - field
        - label
        - operators
        - valueFormat
        - serverEvaluated
        - previewSupport
        - example
    EmailBlockFieldReference:
      type: object
      properties:
        name:
          type: string
          example: "variant"
        required:
          type: boolean
        type:
          type: string
          description: Value shape, for example string, number, boolean, enum, array, or object.
          example: "enum"
        values:
          type: array
          description: Allowed values, for enum and literal-union fields.
          items: {}
          example: ["numbered", "timeline"]
        itemFields:
          type: array
          description: >-
            Shape of one entry in an array field. This is where `list` and
            `steps` differ: list items carry `content`, steps items carry
            `title` and an optional `description`.
          items:
            $ref: "#/components/schemas/EmailBlockFieldReference"
        fields:
          type: array
          description: >-
            Shape of an object field, for example the `mode`, `strategy`,
            `lookbackDays`, `sort`, and `filters` of a repeat block's
            `productSource`.
          items:
            $ref: "#/components/schemas/EmailBlockFieldReference"
    EmailBlock:
      type: object
      description: >-
        Email content block. The shape depends on the block type. Any block may
        carry a `conditions` array so it renders only for matching recipients,
        and the `conditional-group` type adds if/else branching via `ifBranch`
        and `elseBranch` (each an object with a `children` array of blocks).
      required:
        - type
      properties:
        id:
          type: string
          example: "block_123"
        type:
          type: string
          enum:
            - text
            - html
            - heading
            - list
            - button
            - spacer
            - divider
            - image
            - columns
            - conditional-group
            - repeat
            - card
            - cta
            - social
            - logo
            - header
            - footer
            - video
            - product
            - discount-code
            - code
            - countdown
            - hero
            - testimonial
            - gallery
            - badge
            - table
            - features
            - image-card
            - pricing
            - author
            - article
            - rating
            - stats
            - steps
            - product-grid
            - poll
          example: "html"
        content:
          type: string
          description: Content for text, html, and heading-like blocks.
          example: "<h1>Hello</h1>"
        styles:
          type: object
          description: Per-block visual styles. For compatibility, style fields such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius can also be supplied at the block top level and are normalized into this object.
          additionalProperties: false
          properties:
            paddingTop:
              type: number
            paddingBottom:
              type: number
            paddingLeft:
              type: number
            paddingRight:
              type: number
            backgroundColor:
              type: string
              example: "#f8fafc"
            backgroundOpacity:
              type: number
              minimum: 0
              maximum: 100
              description: Background opacity percentage from 0 to 100.
              example: 80
            textColor:
              type: string
              example: "#111827"
            textAlign:
              type: string
              enum:
                - left
                - center
                - right
            borderRadius:
              type: number
              example: 12
            borderColor:
              type: string
              example: "#cbd5e1"
            borderWidth:
              type: number
              example: 1
            bleed:
              type: boolean
              description: Stretch the block edge-to-edge across the email container. Top-level blocks only.
        conditions:
          type: array
          description: >-
            Optional per-block display rules. The block renders only when every
            rule matches. The same shape is used for a conditional-group block's
            top-level `conditions`.
          items:
            type: object
            required:
              - id
              - field
              - operator
              - value
            properties:
              id:
                type: string
                example: "c1"
              field:
                type: string
                enum:
                  - variable
                  - attribute
                  - email
                  - firstName
                  - lastName
                description: >-
                  `variable` resolves a merge-tag path from the transactional
                  send `variables` or an automation `event` payload (nested
                  paths like `order.total` or `event.plan` work). `attribute`
                  reads a stored subscriber attribute. `email`, `firstName`, and
                  `lastName` read core subscriber fields.
              operator:
                type: string
                enum:
                  - is
                  - is_not
                  - contains
                  - not_contains
                  - gt
                  - gte
                  - lt
                  - lte
                  - is_empty
                  - is_not_empty
              value:
                type: string
                description: >-
                  For `variable` and `attribute`, use `name:value` - the part
                  before the colon is the variable path or attribute name, and
                  the part after it is the comparison value. For `email`,
                  `firstName`, and `lastName`, provide the plain comparison
                  string.
                example: "plan:pro"
      additionalProperties: true

    Email:
      type: object
      properties:
        id:
          type: string
          example: "email_123"
        companyId:
          type: string
          example: "company_123"
        name:
          type: string
          example: "Welcome email"
        subject:
          type: string
          example: "Welcome"
        previewText:
          type: string
          nullable: true
          example: "Here's what to do next"
        blocks:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlock"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    EmailComponent:
      type: object
      properties:
        id:
          type: string
          example: "cmp_123"
        companyId:
          type: string
          example: "company_123"
        name:
          type: string
          example: "Default Footer"
        description:
          type: string
          nullable: true
          example: null
        blocks:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlock"
        componentType:
          type: string
          enum: [section, footer]
          example: "footer"
        defaultSlot:
          type: string
          nullable: true
          enum: [footer, null]
          description: Slot this component is pinned as the company default for, or null when it is an ordinary saved component.
          example: "footer"
        version:
          type: integer
          description: Incremented every time the component's blocks are replaced. Emails cloned from an earlier version keep those blocks.
          example: 3
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    EmailSendEvent:
      type: object
      properties:
        id:
          type: string
          example: "evt_123"
        emailSendId:
          type: string
          example: "send_123"
        companyId:
          type: string
          example: "company_123"
        campaignId:
          type: string
          nullable: true
        transactionalEmailId:
          type: string
          nullable: true
        subscriberId:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
          format: email
        emailName:
          type: string
          nullable: true
          example: "Welcome"
        emailType:
          type: string
          nullable: true
          enum: [campaign, transactional, sequence]
          description: Send-source category retained for analytics compatibility. Use deliveryPolicy for the actual marketing or transactional policy.
        deliveryPolicy:
          type: string
          nullable: true
          enum: [marketing, transactional]
          description: Delivery policy used for suppression and compliance behavior, or null when retained legacy events cannot prove it.
        abTestId:
          type: string
          nullable: true
        abTestVariantId:
          type: string
          nullable: true
        eventType:
          type: string
          example: "delivery"
        bounceType:
          type: string
          nullable: true
        bounceSubType:
          type: string
          nullable: true
        complaintType:
          type: string
          nullable: true
        clickedUrl:
          type: string
          nullable: true
        ipAddress:
          type: string
          nullable: true
        userAgent:
          type: string
          nullable: true
        countryCode:
          type: string
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        eventTime:
          type: string
          format: date-time

    EmailSend:
      type: object
      properties:
        id:
          type: string
          example: "send_123"
        companyId:
          type: string
          example: "company_123"
        type:
          type: string
          nullable: true
          enum: [campaign, transactional, sequence]
          description: Send-source category. API/MCP sends use transactional here even when their delivery policy is marketing.
        emailType:
          type: string
          nullable: true
          enum: [marketing, transactional]
          description: Delivery policy used for suppression and compliance behavior, or null when retained legacy data cannot prove it.
        isTransactional:
          type: boolean
          nullable: true
          description: Stored delivery-policy snapshot. Prefer emailType for a normalized value.
        isTestEmail:
          type: boolean
          description: Whether this delivery was a test send hidden from normal sent-email history.
        campaignId:
          type: string
          nullable: true
        transactionalEmailId:
          type: string
          nullable: true
        automationNodeId:
          type: string
          nullable: true
        automationTokenId:
          type: string
          nullable: true
        abTestVariantId:
          type: string
          nullable: true
        subscriberId:
          type: string
          nullable: true
        subscriberExternalId:
          type: string
          nullable: true
          description: Customer-owned subscriber ID captured from a single-recipient transactional send.
        recipientEmail:
          type: string
          nullable: true
          format: email
        additionalRecipients:
          type: object
          nullable: true
          description: Full recipient envelope for multi-recipient transactional sends, as actually sent. Null for single-recipient sends.
          properties:
            to:
              type: array
              items:
                type: string
                format: email
            cc:
              type: array
              items:
                type: string
                format: email
            bcc:
              type: array
              items:
                type: string
                format: email
        subject:
          type: string
          nullable: true
          example: "Welcome"
        senderEmail:
          type: string
          nullable: true
          format: email
        senderName:
          type: string
          nullable: true
        authenticatedDomainId:
          type: string
          nullable: true
        originalReplyTo:
          type: string
          nullable: true
          description: Resolved reply destination, optionally including a display name. When reply tracking is disabled, this is the sent Reply-To header. When reply tracking is enabled, the sent header is a unique trackable address and this value is the forwarding destination.
        sendingIpAddress:
          type: string
          nullable: true
        observedSendingIpAddress:
          type: string
          nullable: true
        sesMessageId:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - pending
            - sent
            - delivered
            - opened
            - clicked
            - bounced
            - complained
            - failed
            - suppressed
        bounceType:
          type: string
          nullable: true
        bounceSubType:
          type: string
          nullable: true
        complaintType:
          type: string
          nullable: true
        suppressionReason:
          type: string
          nullable: true
          enum: [bounced, complaint, unsubscribed]
        errorMessage:
          type: string
          nullable: true
        internalFailureCode:
          type: string
          nullable: true
          enum: [transport_exhausted, admin_bounce, crash_orphaned_claim]
          description: >-
            Why a send reached status "failed". Present only for terminal
            delivery failures. "transport_exhausted" means every available
            delivery route was refused by the recipient provider;
            "admin_bounce" means an operator removed the message from the
            delivery queue; "crash_orphaned_claim" means the sending worker
            stopped between claiming the send and recording a transport
            outcome, so the message was never confirmed as reaching the
            provider. None of them is a bounce: the address is still valid,
            stays subscribed, and is not suppressed. New codes can be added
            over time, so treat an unrecognized value as a generic failure
            rather than rejecting the response.
        emailBody:
          type: string
          nullable: true
          description: Stored HTML body. Null when the email send row has been cleaned up.
        isCopiedRecipient:
          type: boolean
          description: Whether this is an auxiliary CC/BCC delivery record whose content and engagement belong to a primary email send.
        primaryEmailSendId:
          type: string
          nullable: true
          description: Primary email send ID for a copied-recipient delivery, or null for ordinary sends and when the primary record is unavailable.
        sentAt:
          type: string
          format: date-time
          nullable: true
        deliveredAt:
          type: string
          format: date-time
          nullable: true
        bouncedAt:
          type: string
          format: date-time
          nullable: true
        complainedAt:
          type: string
          format: date-time
          nullable: true
        openedAt:
          type: string
          format: date-time
          nullable: true
        clickedAt:
          type: string
          format: date-time
          nullable: true
        unsubscribedAt:
          type: string
          format: date-time
          nullable: true
        suppressedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        subscriber:
          oneOf:
            - type: object
              properties:
                id:
                  type: string
                email:
                  type: string
                  format: email
                firstName:
                  type: string
                  nullable: true
                lastName:
                  type: string
                  nullable: true
            - type: "null"
        campaign:
          oneOf:
            - type: object
              properties:
                id:
                  type: string
                name:
                  type: string
            - type: "null"
        automation:
          oneOf:
            - type: object
              properties:
                id:
                  type: string
                name:
                  type: string
            - type: "null"

    EmailBodyInput:
      type: object
      properties:
        html:
          type: string
          description: Raw HTML body. Provide either html or blocks, not both.
          example: "<h1>Hello</h1>"
        blocks:
          type: array
          description: Structured email blocks. Provide either blocks or html, not both. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
          items:
            $ref: "#/components/schemas/EmailBlock"

    BlockFieldWarnings:
      type: array
      description: >-
        Non-blocking advisories about a successful write. Present when an input
        was discarded or did not take effect as requested. This includes block
        fields that do not render as their names suggest, sequence email-step
        formatting restored on top of submitted blocks, and sender-identity
        conflicts such as a replyToName that differs from the saved profile.
        Each message identifies the affected input and gives recovery guidance.
        Absent when there is nothing to report.
      items:
        type: string
      example:
        - "blocks[0].styles.color is not a supported field and was ignored. Button label color comes from the block-level `buttonTextColor` field. Supported styles fields: backgroundColor, backgroundOpacity, bleed, borderColor, borderRadius, borderWidth, paddingBottom, paddingLeft, paddingRight, paddingTop, textAlign, textColor."

    SequenceStatus:
      type: string
      enum: [draft, active, paused, archived]
      description: >-
        Stored lifecycle status. Do not read literally: a sequence can be active
        while enrollmentPaused blocks new subscribers. Branch on
        effectiveStatus instead.

    SequenceEffectiveStatus:
      type: string
      enum: [draft, live, enrollment_paused, paused, archived]
      description: >-
        Resolved run state, and the single field to branch on. live accepts new
        subscribers and advances existing recipients; enrollment_paused advances
        existing recipients only; draft, paused, and archived do neither.

    SequenceTriggerType:
      type: string
      enum:
        - contact_added
        - tag_added
        - segment_entered
        - event_received
        - inbound_webhook
        - inactivity
        - frequency

    SequenceDelayOffsetInput:
      type: object
      description: Relative offset for a delay.
      properties:
        days:
          type: number
          minimum: 0
          example: 1
        hours:
          type: number
          minimum: 0
          example: 2
        minutes:
          type: number
          minimum: 0
          example: 30

    SequenceDelayInput:
      type: object
      description: Delay before this step runs. Use duration fields for fixed waits, mode until_date with untilDateField for event/date-field waits, or mode until_weekday with the weekday window fields.
      properties:
        mode:
          type: string
          enum:
            - duration
            - until_date
            - until_weekday
          description: Delay mode. Defaults to duration.
          example: "until_date"
        days:
          type: number
          minimum: 0
          example: 1
        hours:
          type: number
          minimum: 0
          example: 2
        minutes:
          type: number
          minimum: 0
          example: 30
        untilDateField:
          type: string
          description: Event/subscriber date field path to wait until when mode is until_date.
          example: "renews_at"
        field:
          type: string
          description: Alias for untilDateField.
          example: "event.subscription.renews_at"
        untilOffsetDirection:
          type: string
          enum:
            - before
            - after
          description: Whether the offset runs before or after the date field. Defaults to after.
          example: "before"
        direction:
          type: string
          enum:
            - before
            - after
          description: Alias for untilOffsetDirection.
          example: "before"
        untilMissingAction:
          type: string
          enum:
            - continue
            - exit
          description: What to do when the date field is missing or invalid. Defaults to continue.
          example: "exit"
        missingAction:
          type: string
          enum:
            - continue
            - exit
          description: Alias for untilMissingAction.
          example: "exit"
        untilDays:
          type: array
          items:
            type: string
          description: Weekdays the wait may release on when mode is until_weekday.
          example: ["sunday"]
        untilStartTime:
          type: string
          description: Window start in 24-hour HH:mm local time when mode is until_weekday.
          example: "09:00"
        untilEndTime:
          type: string
          description: Window end in 24-hour HH:mm local time when mode is until_weekday. Defaults to the end-of-day boundary 24:00.
          example: "12:00"
        untilTimezone:
          type: string
          description: IANA timezone used to evaluate the window when mode is until_weekday.
          example: "America/Los_Angeles"

    SequenceWaitUntilInput:
      type: object
      description: Wait until a date from the enrollment event/subscriber data, optionally offset before or after that date.
      properties:
        field:
          type: string
          description: Event/subscriber date field path to wait until.
          example: "renews_at"
        untilDateField:
          type: string
          description: Alias for field.
          example: "event.subscription.renews_at"
        offset:
          $ref: "#/components/schemas/SequenceDelayOffsetInput"
        days:
          type: number
          minimum: 0
          description: Shorthand offset days when offset is omitted.
          example: 1
        hours:
          type: number
          minimum: 0
          description: Shorthand offset hours when offset is omitted.
          example: 2
        minutes:
          type: number
          minimum: 0
          description: Shorthand offset minutes when offset is omitted.
          example: 30
        direction:
          type: string
          enum:
            - before
            - after
          description: Whether the offset runs before or after the field date. Defaults to after.
          example: "before"
        untilOffsetDirection:
          type: string
          enum:
            - before
            - after
          description: Alias for direction.
          example: "before"
        missingAction:
          type: string
          enum:
            - continue
            - exit
          description: What to do when the date field is missing or invalid. Defaults to continue.
          example: "exit"
        untilMissingAction:
          type: string
          enum:
            - continue
            - exit
          description: Alias for missingAction.
          example: "exit"

    SequenceWaitUntilWeekdayInput:
      type: object
      description: Hold until the next occurrence of a weekday inside a local-time window. Contacts already inside the window continue immediately.
      properties:
        day:
          type: string
          description: Single weekday convenience alias for days.
          example: "sunday"
        days:
          type: array
          items:
            type: string
          description: Weekdays the wait may release on.
          example: ["sunday"]
        startTime:
          type: string
          description: Window start in 24-hour HH:mm local time.
          example: "09:00"
        untilStartTime:
          type: string
          description: Alias for startTime.
          example: "09:00"
        endTime:
          type: string
          description: Window end in 24-hour HH:mm local time. Defaults to the end-of-day boundary 24:00.
          example: "12:00"
        untilEndTime:
          type: string
          description: Alias for endTime.
          example: "12:00"
        timezone:
          type: string
          description: IANA timezone used to evaluate the window.
          example: "America/Los_Angeles"
        untilTimezone:
          type: string
          description: Alias for timezone.
          example: "America/Los_Angeles"

    SequenceDiscountInput:
      type: object
      description: Discount configuration for dynamic create_discount steps.
      properties:
        label:
          type: string
          description: Builder label for discount steps.
          example: "Create win-back discount"
        provider:
          type: string
          enum:
            - stripe
            - shopify
          description: Discount provider. Use stripe to dynamically create a Stripe coupon plus promotion code, or shopify to dynamically create a Shopify Admin discount code.
          example: "stripe"
        discountType:
          type: string
          enum:
            - percent
            - amount
          description: Discount type for create_discount steps.
          example: "percent"
        percentOff:
          type: number
          minimum: 1
          maximum: 100
          description: Percent discount. Required when discountType is percent.
          example: 20
        amountOff:
          type: number
          minimum: 1
          description: Fixed discount amount in the smallest currency unit, for example 500 for $5. Required when discountType is amount.
          example: 500
        currency:
          type: string
          description: ISO currency for fixed-amount discounts. Defaults to usd.
          example: "usd"
        duration:
          type: string
          enum:
            - once
            - forever
            - repeating
          description: Discount duration. Defaults to once.
          example: "once"
        durationInMonths:
          type: number
          minimum: 1
          description: Required for repeating discounts.
          example: 3
        appliesToAllPlans:
          type: boolean
          description: Whether the discount applies to all plans. Defaults to true.
          example: true
        planIds:
          type: array
          description: Provider product IDs when appliesToAllPlans is false. Stripe uses IDs like prod_abc123; Shopify accepts numeric product IDs or gid://shopify/Product/... IDs.
          items:
            type: string
          example: ["prod_abc123"]
        codePrefix:
          type: string
          description: Optional prefix for generated dynamic codes. The final code also includes a subscriber/token suffix.
          example: "SAVE"
        maxRedemptions:
          type: number
          minimum: 1
          description: Maximum redemptions for each generated code. Use 1 for subscriber-specific codes.
          example: 1
        lockToSubscriber:
          type: boolean
          description: Stripe-only. Restrict each generated promotion code to the matched subscriber's Stripe customer.
          example: true
        expiresAt:
          type: string
          description: Optional future expiration date or ISO timestamp. Mutually exclusive with expiresInHours.
          example: "2026-12-31"
        expiresInHours:
          type: number
          minimum: 1
          description: Optional relative expiration in hours, resolved when each subscriber's code is created. Takes precedence over expiresAt.
          example: 48
        name:
          type: string
          description: Optional display name for each dynamically generated provider discount.
          example: "Win-back coupon"

    SequenceStepInput:
      type: object
      properties:
        type:
          type: string
          enum:
            - email
            - sms
            - create_discount
            - discount
            - update_subscriber
          description: Step type. Omit or use email for email content; use sms for a native SMS step; use create_discount for a dynamic discount; use update_subscriber for an Update Subscriber action.
          example: "create_discount"
        nodeType:
          type: string
          enum:
            - action_email
            - action_sms
            - action_create_discount
            - action_update_attributes
          description: Internal node-type alias for clients that work with automation nodes. Use action_update_attributes with config for Update Subscriber steps.
        config:
          $ref: "#/components/schemas/SubscriberUpdateConfig"
        subject:
          type: string
          description: Email subject line for email steps.
          example: "Welcome to Acme"
        previewText:
          type: string
          description: Optional email preview text.
          example: "Here is what to do next"
        text:
          type: string
          description: SMS steps only. Plain-text message body; merge tags like {{FIRST_NAME}} work. Do not include opt-out text or a brand prefix - Sequenzy adds both automatically at send time.
          example: "Hey {{FIRST_NAME}}, your order shipped!"
        imageUrls:
          type: array
          description: SMS steps only. Up to 2 publicly reachable image URLs sent as MMS media.
          items:
            type: string
        ineligibleAction:
          type: string
          enum:
            - skip
            - exit
          description: SMS steps only. skip (default) continues the sequence when the contact can't receive SMS; exit removes them from the sequence.
        blocks:
          type: array
          description: Structured Sequenzy email blocks. Provide either blocks or html. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
          items:
            $ref: "#/components/schemas/EmailBlock"
        html:
          type: string
          description: Raw HTML preserved as one HTML block. Provide either html or blocks.
          example: "<h1>Welcome</h1><p>Thanks for joining.</p>"
        isTransactional:
          type: boolean
          description: Send this email without the marketing unsubscribe footer.
        senderProfileId:
          type: string
          description: Existing sender profile override for this email step.
        fromEmail:
          type: string
          format: email
          description: From address used to create a sender profile for this email step. Mutually exclusive with senderProfileId.
        fromName:
          type: string
          description: Display name override for this email step. Alone it only changes the visible name; with fromEmail it also names a newly created sender profile.
        replyProfileId:
          type: string
          description: Existing reply profile override for this email step.
        replyTo:
          type: string
          format: email
          description: Reply-To address used to create a reply profile for this email step. Mutually exclusive with replyProfileId.
        replyToName:
          type: string
          description: Display name for the step reply profile. Requires replyTo; omit it when using replyProfileId, which already carries its own display name.
        ccEmails:
          type: array
          items:
            type: string
            format: email
          description: Addresses CC'd on this email step. Send an empty array to clear them.
        bccEmails:
          type: array
          items:
            type: string
            format: email
          description: Addresses BCC'd on this email step in addition to sequence-level BCC. Send an empty array to clear them.
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/UrlAttachment"
          description: URL-backed file attachments for this email step, fetched at send time. Event-triggered sequences may use {{event.*}} URL templates. Send an empty array to clear them.
        delay:
          $ref: "#/components/schemas/SequenceDelayInput"
        waitUntil:
          $ref: "#/components/schemas/SequenceWaitUntilInput"
        waitUntilWeekday:
          $ref: "#/components/schemas/SequenceWaitUntilWeekdayInput"
        delayMs:
          type: number
          description: Delay before this step in milliseconds. Prefer delay for human-authored requests; use delayMs when importing provider waits.
          example: 86400000
        name:
          type: string
          description: Optional email template name for email steps.
          example: "Welcome Sequence - Email 1"
        discount:
          allOf:
            - $ref: "#/components/schemas/SequenceDiscountInput"
          description: Discount configuration for create_discount steps. Prefer this nested shape for new integrations; legacy top-level discount fields are still accepted.
        label:
          type: string
          description: Legacy top-level builder label for discount steps. Prefer discount.label.
          example: "Create win-back discount"
        provider:
          type: string
          enum:
            - stripe
            - shopify
          description: Legacy top-level discount provider. Prefer discount.provider. Supports stripe and shopify.
          example: "stripe"
        discountType:
          type: string
          enum:
            - percent
            - amount
          description: Legacy top-level discount type. Prefer discount.discountType.
          example: "percent"
        percentOff:
          type: number
          minimum: 1
          maximum: 100
          description: Percent discount. Required when discountType is percent.
          example: 20
        amountOff:
          type: number
          minimum: 1
          description: Fixed discount amount in the smallest currency unit, for example 500 for $5. Required when discountType is amount.
          example: 500
        currency:
          type: string
          description: ISO currency for fixed-amount discounts. Defaults to usd.
          example: "usd"
        duration:
          type: string
          enum:
            - once
            - forever
            - repeating
          description: Discount duration. Defaults to once.
          example: "once"
        durationInMonths:
          type: number
          minimum: 1
          description: Required for repeating discounts.
          example: 3
        appliesToAllPlans:
          type: boolean
          description: Whether the discount applies to all plans. Defaults to true.
          example: true
        planIds:
          type: array
          description: Provider product IDs when appliesToAllPlans is false. Stripe uses IDs like prod_abc123; Shopify accepts numeric product IDs or gid://shopify/Product/... IDs.
          items:
            type: string
          example: ["prod_abc123"]
        codePrefix:
          type: string
          description: Optional prefix for generated dynamic codes. The final code also includes a subscriber/token suffix.
          example: "SAVE"
        maxRedemptions:
          type: number
          minimum: 1
          description: Maximum redemptions for each generated code. Use 1 for subscriber-specific codes.
          example: 1
        lockToSubscriber:
          type: boolean
          description: Legacy top-level Stripe-only flag. Prefer discount.lockToSubscriber.
          example: true
        expiresAt:
          type: string
          description: Optional future expiration date or ISO timestamp. Mutually exclusive with expiresInHours.
          example: "2026-12-31"
        expiresInHours:
          type: number
          minimum: 1
          description: Optional relative expiration in hours, resolved when each subscriber's code is created. Takes precedence over expiresAt.
          example: 48

    SequenceStopCondition:
      type: object
      description: >-
        Auto-stop condition, re-evaluated before every step including the first
        one. has_tag, added_to_list, entered_segment, field_changed, and
        event_received stop the run once the thing happens. event_received only
        counts events received after enrollment - the enrolling event and
        earlier history never satisfy the stop. does_not_have_tag
        and removed_from_list stop the run whenever the subscriber lacks that
        tag or list membership, so they act as a required-tag or required-list
        allowlist and cancel everyone else before any step sends. Guarded-out
        contacts still enroll and are then cancelled at the trigger node, so
        they appear as cancellations there rather than in the active or waiting
        enrollment counts. Clearing the guard does not retry them: they only
        receive the sequence if the trigger fires for them again, and on the
        one_time enrollment mode not even then.
      properties:
        type:
          type: string
          enum:
            - none
            - has_tag
            - does_not_have_tag
            - added_to_list
            - removed_from_list
            - entered_segment
            - field_changed
            - event_received
        value:
          type: string
          nullable: true
          description: >-
            Tag name, list ID, segment ID, field path, or event name. For the
            does_not_have_tag and removed_from_list guards this is the tag or
            list a subscriber must have to keep receiving the sequence. Null
            with entry_audience matching means the tag or list that enrolled
            each contact is used.
          example: "saas.purchase"
        matchConfig:
          nullable: true
          description: >-
            Optional typed match rule. event_received uses event_property_filter
            propertyFilters (stop only when an event received after enrollment
            matches every filter, e.g. quota_used greater_than 1) or
            event_property rules (stop only when the stop event's field equals
            the same field captured on the enrolling event); field_changed uses
            a field_value comparison. Tag/list defaults use
            entry_audience to resolve the required tag or list per enrollment.
            Tag entry matching requires a tag_added trigger; list entry matching
            requires a contact_added trigger scoped to at least one specific list.
          oneOf:
            - type: object
              required: [mode, propertyFilters]
              properties:
                mode:
                  type: string
                  enum: [event_property_filter]
                propertyFilters:
                  type: array
                  description: >-
                    Filters an event received after enrollment must all match
                    for the stop to fire. Same shape as event trigger
                    propertyFilters.
                  items:
                    type: object
                    required: [path, operator]
                    properties:
                      path:
                        type: string
                        description: Dot-path into the stop event's properties.
                        example: "quota_used"
                      operator:
                        type: string
                        enum:
                          [
                            exists,
                            not_exists,
                            equals,
                            not_equals,
                            one_of,
                            contains,
                            greater_than,
                            less_than,
                          ]
                      value:
                        description: >-
                          Comparison value (string, number, or boolean; array of
                          values for one_of). Omit for exists/not_exists.
            - type: object
              required: [mode, rules]
              properties:
                mode:
                  type: string
                  enum: [event_property]
                rules:
                  type: array
                  items:
                    type: object
                    required: [entryFieldPath, eventFieldPath]
                    properties:
                      entryFieldPath:
                        type: string
                      eventFieldPath:
                        type: string
            - type: object
              required: [mode, operator, value]
              properties:
                mode:
                  type: string
                  enum: [field_value]
                operator:
                  type: string
                  enum:
                    [
                      equals,
                      not_equals,
                      greater_than,
                      less_than,
                      contains,
                      not_contains,
                    ]
                value:
                  type: string
            - type: object
              required: [mode, audience]
              properties:
                mode:
                  type: string
                  enum: [entry_audience]
                audience:
                  type: string
                  enum: [tag, list]
                  description: Use the tag or list recorded when this contact enrolled.

    SequenceEnrollmentMode:
      type: string
      enum:
        - unlimited
        - one_time
        - matching_field
      description: |
        Controls sequence re-entry. `unlimited` allows re-entry after completion but prevents concurrent active runs. `one_time` allows one enrollment ever. `matching_field` is only valid for event-triggered sequences and allows concurrent active runs only when the trigger payload resolves to a different event field value.

    SequenceSendingWindow:
      type: object
      description: Optional local-time sending window applied to every email step in a sequence. Email steps that become due outside the window wait until the next allowed local time.
      properties:
        enabled:
          type: boolean
          example: true
        timezone:
          type: string
          description: IANA timezone for the window.
          example: "Europe/Kiev"
        startTime:
          type: string
          pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
          description: Earliest local send time in 24-hour HH:mm format.
          example: "08:00"
        endTime:
          type: string
          pattern: "^(([01][0-9]|2[0-3]):[0-5][0-9]|24:00)$"
          description: Latest local send cutoff in 24-hour HH:mm format, or 24:00 for the end-of-day boundary. Must be later than startTime.
          example: "20:00"
        days:
          type: array
          description: Allowed local days. Omit days when creating or updating to allow every day.
          items:
            type: string
            enum:
              - sunday
              - monday
              - tuesday
              - wednesday
              - thursday
              - friday
              - saturday
          example: ["monday", "tuesday", "wednesday", "thursday", "friday"]

    SequenceTriggerPropertyFilter:
      type: object
      required:
        - path
        - operator
      properties:
        path:
          type: string
          description: Dot-path into the event properties. Use [] to match items inside arrays.
          example: "lineItems[].providerProductId"
        operator:
          type: string
          enum:
            - exists
            - not_exists
            - equals
            - not_equals
            - one_of
            - contains
            - greater_than
            - less_than
          description: Comparison operator. Value is required for every operator except exists and not_exists. `one_of` matches when the property equals any entry of the value array.
          example: "equals"
        value:
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: array
              maxItems: 50
              items:
                oneOf:
                  - type: string
                  - type: number
          description: Value to compare against. For `one_of`, pass a non-empty array of strings or numbers (maximum 50 values); all other operators take a single value.
          example: "prod_ABC123"

    SequenceCreateRequest:
      type: object
      required:
        - name
      properties:
        description:
          type: string
          description: Optional dashboard description.
        userCancellable:
          type: boolean
          description: Whether recipients can cancel this sequence from email preferences.
        labels:
          type: array
          items:
            type: string
          description: Dashboard label names. Missing labels are created.
        bccEmails:
          type: array
          nullable: true
          maxItems: 10
          items:
            type: string
            format: email
          description: Addresses blind-copied on every sequence email.
        fromEmail:
          type: string
          format: email
          description: From address for every email in this sequence. Its domain must be configured and verified.
        fromName:
          type: string
          description: Display name recipients see, e.g. 'Brennon at TradeTally'. Selects the sender identity of that name on fromEmail, creating it when the address has no identity by that name; the mailbox's other display names, and everything pinned to them, are untouched. Requires fromEmail; omit it when using senderProfileId, which already carries its own display name.
        senderProfileId:
          type: string
          description: Existing sender profile ID. It already supplies both the From address and display name, so send it on its own and omit fromEmail and fromName. To keep this profile under a different display name, set fromName on the email steps instead, where it is a per-step override.
        replyTo:
          type: string
          format: email
          description: Reply-To address for every email in this sequence. A profile is created when needed.
        replyToName:
          type: string
          description: Display name for the Reply-To address. Requires replyTo; omit it when using replyProfileId, which already carries its own display name. An address carries one Reply-To name company-wide, so if replyTo already has a saved profile under a different name, that saved name is kept and the response `warnings` array says so.
        replyProfileId:
          type: string
          description: Existing reply profile ID. It already supplies both the Reply-To address and display name, so send it on its own and omit replyTo and replyToName.
        name:
          type: string
          example: "Welcome Sequence"
        trigger:
          $ref: "#/components/schemas/SequenceTriggerType"
          description: Defaults to contact_added when omitted.
        listId:
          type: string
          description: List ID for contact_added triggers. Omit it to use listScope instead. Use listIds to trigger on several lists.
          example: "list_abc123"
        listIds:
          type: array
          minItems: 1
          maxItems: 25
          items:
            type: string
          description: >-
            Several list IDs for a contact_added trigger. A contact joining ANY
            of them enrolls. Takes precedence over listId when both are sent.
            Cannot be combined with listScope.
          example: ["list_abc123", "list_def456"]
        listScope:
          type: string
          enum: [any_contact, any_list]
          default: any_contact
          description: >-
            For contact_added triggers with no list at all. `any_contact` (the
            default) enrolls every contact added, including contacts that join
            no list - which is what integrations create when list targeting is
            empty. `any_list` waits until the contact joins a list. Cannot be
            combined with listId or listIds.
          example: "any_list"
        tagName:
          type: string
          description: Tag name for tag_added triggers. Use tagNames to trigger on several tags.
          example: "trial"
        tagNames:
          type: array
          minItems: 1
          maxItems: 25
          items:
            type: string
          description: >-
            Several tag names for a tag_added trigger. Receiving ANY of them
            enrolls the contact. Takes precedence over tagName when both are
            sent.
          example: ["trial", "vip"]
        segmentId:
          type: string
          description: Segment ID for segment_entered triggers.
          example: "seg_abc123"
        stopOnSegmentExit:
          type: boolean
          description: For segment_entered triggers, cancel enrollment when the subscriber leaves the segment.
        eventName:
          type: string
          description: Event name for event_received, inbound_webhook, inactivity, and frequency triggers.
          example: "onboarding.started"
        integrationSlug:
          type: string
          description: Integration slug for inbound_webhook triggers.
        integrationEventKey:
          type: string
          description: Integration event key for inbound_webhook triggers.
        customIntegration:
          type: object
          additionalProperties: true
          description: Custom inbound-webhook integration metadata.
        propertyFilters:
          type: array
          maxItems: 10
          description: Event property filters for event_received and inbound_webhook triggers. The sequence only starts when the triggering event's properties match all filters. Use [] in the path to match items inside arrays.
          items:
            $ref: "#/components/schemas/SequenceTriggerPropertyFilter"
        inactiveDays:
          type: number
          minimum: 1
          description: Days of inactivity before the sequence starts.
          example: 14
        inactivityBaseline:
          type: string
          enum:
            - sequence_created_at
            - subscriber_created_at
          description: For inactivity triggers, controls when to start counting for subscribers who have never performed the event. Defaults to sequence_created_at.
          example: "sequence_created_at"
        minCount:
          type: number
          minimum: 1
          description: Minimum event count for frequency triggers.
          example: 3
        timeWindowDays:
          type: number
          minimum: 1
          description: Time window in days for frequency triggers.
          example: 7
        enrollmentMode:
          $ref: "#/components/schemas/SequenceEnrollmentMode"
        enrollmentFieldPath:
          type: string
          description: Scalar dot-path event property used by matching_field enrollment, such as order.id or product.providerVariantId. Array traversal with [] is not supported; use propertyFilters for array matching. Applies to event_received and inbound_webhook triggers. Leave empty for built-in Shopify product/variant defaults.
          example: "order.id"
        sendingWindow:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SequenceSendingWindow"
        emailCount:
          type: number
          minimum: 1
          maximum: 10
          description: Number of emails for AI-generated content. Defaults to 5. Maximum is 10.
          example: 5
        durationDays:
          type: number
          minimum: 1
          description: Total duration in days used to space AI-generated emails. Omit this to use the default sequence delay schedule.
          example: 14
        emailStyle:
          type: string
          enum:
            - visual
            - plain
          description: "Style for the AI-generated emails: visual (designed, with heroes/imagery/rich sections) or plain (personal, text-first notes with a single button). Defaults to the company's saved preference when omitted."
          example: visual
        goal:
          type: string
          description: Goal for AI-generated sequence content. Provide either goal or steps, or omit both for a blank dashboard-compatible draft.
          example: "Welcome new users and help them reach activation."
        stopCondition:
          $ref: "#/components/schemas/SequenceStopCondition"
        steps:
          type: array
          description: Explicit email and action steps. Provide either steps or goal, or omit both for a blank dashboard-compatible draft.
          items:
            $ref: "#/components/schemas/SequenceStepInput"

    SequenceEmailUpdateInput:
      type: object
      properties:
        emailId:
          type: string
          description: Email template ID to update. You can also pass the node ID here for compatibility.
          example: "email_abc123"
        nodeId:
          type: string
          description: Sequence email node ID to update.
          example: "node_abc123"
        name:
          type: string
          description: Updated step and email template name.
          example: "Welcome Sequence - Email 1"
        subject:
          type: string
          description: Updated subject line.
          example: "Welcome to Acme"
        previewText:
          type: string
          nullable: true
          description: Updated preview text.
          example: "Start here"
        isTransactional:
          type: boolean
          description: Use transactional email chrome for this sequence email.
        ccEmails:
          type: array
          items:
            type: string
            format: email
        bccEmails:
          type: array
          items:
            type: string
            format: email
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/UrlAttachment"
          description: URL-backed file attachments for this email step, fetched at send time. Event-triggered sequences may use {{event.*}} URL templates. Send an empty array to clear them.
        emailPreset:
          type: string
          enum:
            - branded
            - minimal
          description: Per-email Style > Format for native Sequenzy blocks, including emails that contain supported custom HTML blocks. Minimal removes the company logo and uses the simple footer; branded restores the branded chrome. This is not supported when the entire email is standalone raw HTML and must not be combined with html or htmlContent.
          example: minimal
        emailTheme:
          allOf:
            - $ref: "#/components/schemas/EmailThemePatch"
          description: >-
            Theme override for this step's linked email only. Partial patch
            merged into the email's current theme; a step with no override
            merges into the company theme. Null drops the override.
        blocks:
          type: array
          description: >-
            Replacement Sequenzy email blocks. Put visual styling under styles;
            top-level style keys such as backgroundColor, backgroundOpacity,
            borderColor, borderWidth, and borderRadius are normalized into
            styles. Replacing blocks keeps that step's existing Style > Format
            rather than the company default: a step that already had a logo or a
            footer gets them back even when you omit them, and every block added
            that way is named in the response warnings array. Send emailPreset to
            change the format instead. A step stored as one standalone raw HTML
            block has no format, so replacing it with another standalone raw HTML
            block stores it exactly as sent and nothing is added; replacing that
            markup with native blocks is a conversion rather than a markup edit,
            so the new blocks do get a footer.
          items:
            $ref: "#/components/schemas/EmailBlock"
        html:
          type: string
          description: Raw HTML preserved as one HTML block. Provide html/htmlContent or blocks.
          example: "<p>Updated body</p>"
        htmlContent:
          type: string
          description: Alias for html. Raw HTML is preserved as one HTML block.
          example: "<p>Updated body</p>"
        senderProfileId:
          type: string
          description: Sender profile for this step's From identity. Overrides the sequence-level sender for this step. Mutually exclusive with fromEmail.
          example: "sender_abc123"
        fromEmail:
          type: string
          format: email
          description: From address for this step. Its domain must be configured and verified. Mutually exclusive with senderProfileId.
        fromName:
          type: string
          description: Display name override for this step. Alone it only changes the visible name; with fromEmail it also names a newly created sender profile.
          example: "Michael from Acme"
        replyProfileId:
          type: string
          description: Reply profile for this step's Reply-To. Mutually exclusive with replyTo.
          example: "reply_abc123"
        replyTo:
          type: string
          format: email
          description: Reply-To address for this step. Mutually exclusive with replyProfileId.
        replyToName:
          type: string
          description: Reply-To display name override for this step. Requires replyTo; omit it when using replyProfileId, which already carries its own display name.

    SequenceBranchConditionInput:
      type: object
      properties:
        id:
          type: string
          description: Optional stable branch ID. Defaults to branch-0, branch-1, etc.
          example: "branch-0"
        label:
          type: string
          example: "Has activated tag"
        conditionType:
          type: string
          enum:
            - has_tag
            - does_not_have_tag
            - in_list
            - in_segment
            - event_received
            - link_clicked
            - field_equals
            - field_contains
            - field_greater_than
            - field_less_than
            - has_phone
            - sms_subscribed
          description: Condition for this path. Required on a condition split; must be omitted when the branch sets splitMode to random, where paths are chosen by percentage.
          example: "has_tag"
        tagId:
          type: string
          description: Tag ID or tag name for has_tag and does_not_have_tag conditions.
          example: "activated"
        tagName:
          type: string
          description: Tag name for has_tag and does_not_have_tag conditions.
          example: "activated"
        listId:
          type: string
          description: List ID for in_list conditions.
          example: "list_abc123"
        segmentId:
          type: string
          description: Segment ID for in_segment conditions.
          example: "seg_abc123"
        segmentName:
          type: string
          description: Optional display name for in_segment conditions.
          example: "Project invite recipients"
        eventName:
          type: string
          description: Event name for event_received conditions.
          example: "project.invite.accepted"
        linkUrl:
          type: string
          description: Optional URL substring for link_clicked conditions. Omit to match any clicked link.
          example: "project-invites"
        activityScope:
          type: string
          enum:
            - ever
            - this_sequence
            - previous_email
          description: Scope for event_received and link_clicked conditions.
          example: "this_sequence"
        fieldName:
          type: string
          description: Subscriber attribute name for field conditions.
          example: "plan"
        fieldValue:
          type: string
          description: Comparison value for field conditions.
          example: "pro"
        targetNodeId:
          type: string
          description: Existing node reached by this branch path after any newly created steps. Use the completion node ID to end this path immediately.
          example: "node_sequence_complete"
        steps:
          type: array
          description: Optional steps to create in this branch path. When targetNodeId is also set, the final new step connects to that existing node.
          items:
            $ref: "#/components/schemas/SequenceBranchPathStepInput"

    SequenceBranchPathStepInput:
      type: object
      description: Step created inside a sequence path.
      properties:
        type:
          type: string
          enum:
            - email
            - sms
            - delay
            - create_discount
            - discount
            - update_subscriber
            - condition
            - webhook
            - ai
          description: Step type. Omit for email steps, use sms for a native SMS step, or use delay for a standalone wait.
          example: "email"
        nodeType:
          type: string
          enum:
            - logic_delay
            - action_email
            - action_sms
            - action_create_discount
            - action_add_tag
            - action_remove_tag
            - action_add_to_list
            - action_remove_from_list
            - action_update_attributes
            - logic_wait_for_event
            - logic_condition
            - action_webhook
            - action_ai
          description: Advanced node type for non-email sequence path actions.
        config:
          $ref: "#/components/schemas/SequencePathStepConfig"
        subject:
          type: string
          description: Email subject. Required for email steps.
          example: "Can you refer us?"
        previewText:
          type: string
          nullable: true
          description: Email preview text.
        text:
          type: string
          description: SMS steps only. Plain-text message body; merge tags like {{FIRST_NAME}} work.
          example: "Hey {{FIRST_NAME}}, your order shipped!"
        imageUrls:
          type: array
          description: SMS steps only. Up to 2 publicly reachable image URLs sent as MMS media.
          items:
            type: string
        ineligibleAction:
          type: string
          enum:
            - skip
            - exit
          description: SMS steps only. skip (default) continues the sequence when the contact can't receive SMS; exit removes them from the sequence.
        label:
          type: string
          description: SMS steps only. Display label for the step in the builder.
          example: "Shipping text"
        blocks:
          type: array
          description: Email blocks for email steps. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
          items:
            $ref: "#/components/schemas/EmailBlock"
        html:
          type: string
          description: HTML content for email steps.
          example: "<p>Can you refer us?</p>"
        delay:
          $ref: "#/components/schemas/SequenceDelayInput"
        waitUntil:
          $ref: "#/components/schemas/SequenceWaitUntilInput"
        waitUntilWeekday:
          $ref: "#/components/schemas/SequenceWaitUntilWeekdayInput"
        delayMs:
          type: number
          description: Delay in milliseconds. Useful for standalone delay steps.
          example: 86400000
        name:
          type: string
          description: Email template name for email steps.
        senderProfileId:
          type: string
          description: Email steps only. Sender profile for the new step. Omit to inherit the sender identity of the email step it is inserted after. Mutually exclusive with fromEmail.
        fromEmail:
          type: string
          format: email
          description: Email steps only. From address for the new step; its domain must be verified. Mutually exclusive with senderProfileId.
        fromName:
          type: string
          description: Email steps only. Display name override for the new step. With fromEmail, also names a newly created sender profile.
        replyProfileId:
          type: string
          description: Email steps only. Reply profile for the new step. Omit to inherit from the preceding email step. Mutually exclusive with replyTo.
        replyTo:
          type: string
          format: email
          description: Email steps only. Reply-To address for the new step. Mutually exclusive with replyProfileId.
        replyToName:
          type: string
          description: Email steps only. Reply-To display name override for this step. Requires replyTo; omit it when using replyProfileId, which already carries its own display name.
        discount:
          type: object
          description: Discount configuration for create_discount steps.
          additionalProperties: true

    SequenceBranchInput:
      type: object
      required:
        - afterNodeId
        - branches
      properties:
        afterNodeId:
          type: string
          description: Existing sequence node ID to insert the branch after.
          example: "node_trigger"
        label:
          type: string
          example: "Branching"
        splitMode:
          type: string
          enum:
            - condition
            - random
          default: condition
          description: How subscribers are routed. condition evaluates each branch's conditionType in order. random assigns each subscriber a path by percentage when they reach the node, which runs a concurrent A/B test inside the sequence. A random split requires randomPercentages, omits conditionType and condition-specific fields on every branch, and has no else path.
        randomPercentages:
          type: array
          items:
            type: number
          description: Required when splitMode is random. One non-negative percentage per entry in branches, in the same order, summing to 100.
          example: [50, 50]
        branches:
          type: array
          description: Branch paths. On a condition split they are evaluated in order and an else fallback is created automatically. On a random split each entry is a weighted variant. Each branch should include steps, targetNodeId, or both unless allowEmptyPaths is true.
          items:
            $ref: "#/components/schemas/SequenceBranchConditionInput"
        elseSteps:
          type: array
          description: Optional steps to create in the else fallback path. When elseTargetNodeId is also set, the final new step connects to that existing node. Rejected when splitMode is random.
          items:
            $ref: "#/components/schemas/SequenceBranchPathStepInput"
        elseTargetNodeId:
          type: string
          description: Existing node reached by the else fallback path after any elseSteps. Use the original follow-up node to keep that path in the existing flow, or the completion node to end it. Rejected when splitMode is random.
          example: "node_follow_up"
        allowEmptyPaths:
          type: boolean
          description: Set true only when intentionally creating empty UI placeholders. Explicit target node paths do not require this flag.
          default: false

    SequenceLinearStepInsertionInput:
      type: object
      required:
        - steps
      properties:
        afterNodeId:
          type: string
          description: Existing sequence node ID to insert after. Omit only to append to an unambiguous linear tail.
          example: "node_migration_email"
        steps:
          type: array
          description: New linear steps to insert. Supports addable step types that do not require companion records; multi-path branches use the branch payload. Email steps require subject plus blocks or html. Inserted email steps inherit the effective identity of the nearest sequence email unless the step sets its own sender fields. After a branch merge, only identity fields shared by every incoming path are inherited; conflicting fields use sequence or company defaults.
          items:
            $ref: "#/components/schemas/SequenceBranchPathStepInput"

    SequenceGraphEdgeInput:
      type: object
      required:
        - sourceNodeId
        - targetNodeId
      properties:
        sourceNodeId:
          type: string
          description: Existing source node ID.
          example: "node_trigger"
        targetNodeId:
          type: string
          description: Existing target node ID.
          example: "node_welcome_email"
        condition:
          type: object
          nullable: true
          additionalProperties: true
          description: Exact branch-lane condition for this edge. Omit or set null for an unconditional edge.
          example:
            branchId: "branch-0"

    SequenceGraphEditInput:
      type: object
      required:
        - action
        - expectedRevision
      properties:
        action:
          type: string
          enum: [move_node, delete_node, duplicate_node, replace_edges]
          description: Structural graph operation to perform.
        expectedRevision:
          type: string
          description: graphRevision from the latest get-sequence response. The update is rejected if the graph changed after it was read.
          example: "75afbbfe05c1a96dc4f167eecc80b14113a3b76b47fb1b193492ddcd2b1d3e21"
        nodeId:
          type: string
          description: Existing node to move, delete, or duplicate.
        afterNodeId:
          type: string
          description: Insert the moved or duplicated node immediately after this node. Mutually exclusive with beforeNodeId.
        beforeNodeId:
          type: string
          description: Insert the moved or duplicated node immediately before this node. Mutually exclusive with afterNodeId.
        edges:
          type: array
          description: Complete replacement topology for replace_edges. Also supported when deleting a split node whose continuation cannot be inferred safely.
          items:
            $ref: "#/components/schemas/SequenceGraphEdgeInput"

    EmailThemePatch:
      type: object
      nullable: true
      additionalProperties: false
      description: >-
        Partial visual theme patch. Omitted fields keep their current value, so
        {"colors": {"background": "#ffffff"}} repaints only the background.
        Colors are 6-digit hex; numeric values are clamped to their supported
        ranges. Null clears the stored theme.
      properties:
        presetId:
          type: string
          enum:
            - default
            - soft
            - editorial
            - bold
        buttonStyle:
          type: string
          enum:
            - solid
            - outline
        colors:
          type: object
          additionalProperties: false
          properties:
            primary:
              type: string
            background:
              type: string
            surface:
              type: string
            text:
              type: string
            mutedText:
              type: string
            heading:
              type: string
            border:
              type: string
            link:
              type: string
            buttonText:
              type: string
        typography:
          type: object
          additionalProperties: false
          properties:
            baseFontSize:
              type: number
            leadFontSize:
              type: number
            baseLineHeight:
              type: number
            heading1Size:
              type: number
            heading2Size:
              type: number
            heading3Size:
              type: number
            buttonFontSize:
              type: number
            buttonFontWeight:
              type: number
            headingFontWeight:
              type: number
            headingFontFamily:
              type: string
            headingLetterSpacing:
              type: number
        layout:
          type: object
          additionalProperties: false
          properties:
            contentWidth:
              type: number
            containerPaddingX:
              type: number
            containerPaddingY:
              type: number
            blockSpacing:
              type: number
            baseRadius:
              type: number
            buttonRadius:
              type: number
            sectionPadding:
              type: number
            buttonPaddingX:
              type: number
            buttonPaddingY:
              type: number
            borderedBlockPadding:
              type: number

    SequenceNodeUpdateInput:
      type: object
      required:
        - nodeId
        - changes
      properties:
        nodeId:
          type: string
          description: Existing sequence node ID returned by get sequence.
          example: "node_abc123"
        changes:
          type: object
          minProperties: 1
          additionalProperties: true
          description: >-
            Type-aware node patch. Use the editableFields and notes from the
            node's updateHints. Omitted fields are preserved. For action_email
            nodes, a blocks patch that omits emailPreset never changes the step's
            format - see blocks on SequenceEmailUpdateInput.
          properties:
            emailPreset:
              type: string
              enum:
                - branded
                - minimal
              description: For action_email nodes, set the linked email's per-email Style > Format. Native block emails may include supported custom HTML blocks. Not supported when the entire email is standalone raw HTML.
            emailTheme:
              allOf:
                - $ref: "#/components/schemas/EmailThemePatch"
              description: >-
                For action_email nodes, override this step's linked email theme
                only. The company default and every other email are untouched.
                The patch merges into the email's current theme, and a step with
                no override merges into the company theme rather than the
                platform preset. Send null to drop the override so the step
                follows the company theme again.
        expectedUpdatedAt:
          type: string
          format: date-time
          description: Optional optimistic-concurrency token from the node's latest updatedAt or updateHints.expectedUpdatedAt value. MCP clients require this field.
          example: "2026-07-17T10:00:00.000Z"

    SequenceUpdateRequest:
      type: object
      properties:
        description:
          type: string
          description: Updated dashboard description.
        userCancellable:
          type: boolean
          description: Whether recipients can cancel this sequence from email preferences.
        labels:
          type: array
          items:
            type: string
          description: Replacement dashboard label names. Missing labels are created.
        fromEmail:
          type: string
          format: email
          description: From address for every email in this sequence. Its domain must be configured and verified.
        fromName:
          type: string
          description: Display name recipients see, e.g. 'Brennon at TradeTally'. Selects the sender identity of that name on fromEmail, creating it when the address has no identity by that name; the mailbox's other display names, and everything pinned to them, are untouched. Requires fromEmail; omit it when using senderProfileId, which already carries its own display name.
        senderProfileId:
          type: string
          description: Existing sender profile ID. It already supplies both the From address and display name, so send it on its own and omit fromEmail and fromName. To keep this profile under a different display name, set fromName on the email steps instead, where it is a per-step override.
        replyTo:
          type: string
          format: email
          description: Reply-To address for every email in this sequence.
        replyToName:
          type: string
          description: Display name for the Reply-To address. Requires replyTo; omit it when using replyProfileId, which already carries its own display name. An address carries one Reply-To name company-wide, so if replyTo already has a saved profile under a different name, that saved name is kept and the response `warnings` array says so.
        replyProfileId:
          type: string
          description: Existing reply profile ID. It already supplies both the Reply-To address and display name, so send it on its own and omit replyTo and replyToName.
        name:
          type: string
          description: Updated sequence name.
          example: "Updated Welcome Sequence"
        trigger:
          $ref: "#/components/schemas/SequenceTriggerType"
          description: Atomically replaces the current trigger. Include its typed configuration fields in the same request. Active sequences require confirmLiveChange.
        listId:
          type: string
          description: List ID for a replacement contact_added trigger.
        listIds:
          type: array
          minItems: 1
          maxItems: 25
          items:
            type: string
          description: >-
            Several list IDs for a replacement contact_added trigger. A contact
            joining ANY of them enrolls. Cannot be combined with listScope.
        listScope:
          type: string
          enum: [any_contact, any_list]
          default: any_contact
          description: >-
            For a replacement contact_added trigger with no list.
            `any_contact` (the default) enrolls every contact added, even one
            that joins no list; `any_list` waits for a list membership. Cannot
            be combined with listId or listIds.
        tagName:
          type: string
          description: Tag name for a replacement tag_added trigger.
        tagNames:
          type: array
          minItems: 1
          maxItems: 25
          items:
            type: string
          description: >-
            Several tag names for a replacement tag_added trigger. Receiving
            ANY of them enrolls the contact.
        segmentId:
          type: string
          description: Segment ID for a replacement segment_entered trigger.
        stopOnSegmentExit:
          type: boolean
          description: Stop active enrollments when a contact leaves the replacement trigger segment.
        eventName:
          type: string
          description: Event name for event_received, inbound_webhook, inactivity, or frequency triggers.
        propertyFilters:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/SequenceTriggerPropertyFilter"
        integrationSlug:
          type: string
          description: Catalog integration slug for an inbound_webhook trigger.
        integrationEventKey:
          type: string
          description: Catalog event key for an inbound_webhook trigger.
        customIntegration:
          type: object
          additionalProperties: true
          description: Custom integration descriptor for an inbound_webhook trigger.
        inactiveDays:
          type: number
          minimum: 1
        inactivityBaseline:
          type: string
          enum: [sequence_created_at, subscriber_created_at]
        minCount:
          type: number
          minimum: 1
        timeWindowDays:
          type: number
          minimum: 1
        enrollmentPaused:
          type: boolean
          description: Set true to stop new enrollments for an active sequence while current recipients continue. Set false to resume new enrollments.
          example: true
        confirmStructuralChange:
          type: boolean
          description: Required when inserting steps or branches, or editing the graph of an active sequence. Set true only after confirming the live-flow impact for current and future recipients.
          example: true
        confirmLiveChange:
          type: boolean
          description: Required for trigger replacement or nodeUpdates on an active sequence. Set true only after confirming that the edits can affect recipients who reach those nodes in the future.
          example: true
        enrollmentMode:
          $ref: "#/components/schemas/SequenceEnrollmentMode"
        enrollmentFieldPath:
          type: string
          nullable: true
          description: Scalar dot-path event property used by matching_field enrollment on event_received and inbound_webhook sequences. Array traversal with [] is not supported; use propertyFilters for array matching. Set to null to use built-in defaults.
          example: "order.id"
        sendingWindow:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SequenceSendingWindow"
        bccEmails:
          type: array
          nullable: true
          items:
            type: string
          description: Email addresses that receive a blind copy of every email this sequence sends, such as a customer support inbox (max 10). Set to null to remove them.
          example: ["cs-team@example.com", "founder@example.com"]
        stopCondition:
          $ref: "#/components/schemas/SequenceStopCondition"
        branch:
          $ref: "#/components/schemas/SequenceBranchInput"
        insertSteps:
          $ref: "#/components/schemas/SequenceLinearStepInsertionInput"
        graphEdit:
          $ref: "#/components/schemas/SequenceGraphEditInput"
        nodeUpdates:
          type: array
          minItems: 1
          description: Atomic, type-aware patches for existing sequence nodes. A node may appear only once, and either every patch commits or none do.
          items:
            $ref: "#/components/schemas/SequenceNodeUpdateInput"
        emails:
          type: array
          description: Existing email step updates. Provide either emails or steps. Items without nodeId or emailId are matched by existing step order and do not create new steps.
          items:
            $ref: "#/components/schemas/SequenceEmailUpdateInput"
        steps:
          type: array
          description: Alias for emails. Use insertSteps to create new steps.
          items:
            $ref: "#/components/schemas/SequenceEmailUpdateInput"
        smsSteps:
          type: array
          description: Content updates for existing SMS steps, targeted by action_sms nodeId. Content-only edits; use insertSteps to create new SMS steps.
          items:
            $ref: "#/components/schemas/SequenceSmsStepUpdateInput"
        subscriberUpdateSteps:
          type: array
          description: Full config replacements for existing action_update_attributes steps, targeted by nodeId.
          items:
            $ref: "#/components/schemas/SequenceSubscriberUpdateStepUpdateInput"

    SubscriberUpdateConfig:
      type: object
      description: Update Subscriber config. String values may use standalone trigger-event merge tags such as {{event.plan}}, {{event.amount}}, or {{event.active}}. Number and boolean values are coerced after resolution.
      additionalProperties: true
      properties:
        label:
          type: string
        firstName:
          type: string
          nullable: true
        lastName:
          type: string
          nullable: true
        status:
          type: string
          enum: [active, unsubscribed, bounced]
        customAttributeUpdates:
          type: array
          items:
            type: object
            required: [name, value]
            properties:
              name:
                type: string
              value:
                nullable: true
                description: Literal scalar, null to delete, or one standalone merge tag.
                oneOf:
                  - type: string
                  - type: number
                  - type: boolean
              valueType:
                type: string
                enum: [text, number, boolean]

    SequencePathStepConfig:
      type: object
      description: >-
        Config for advanced nodeType steps. Required fields depend on nodeType:
        action_add_tag and action_remove_tag need tagId or tagName;
        action_add_to_list and action_remove_from_list need listId;
        action_update_attributes uses the Update Subscriber fields and may reference the trigger event payload;
        logic_wait_for_event needs eventName plus optional timeoutDays and timeoutAction;
        logic_condition needs conditionType plus that condition's resource field;
        action_webhook needs an HTTPS url plus optional method, headers, body, resultKey, and onError;
        action_ai needs prompt, resultKey, and outputFields plus optional includeTags, includeEventProperties, includeRecentEvents, recentEventLimit, includeAttributes, and onError;
        logic_delay uses delayDays, delayHours, and delayMinutes.
        Fields that do not apply to the node type are dropped.
      allOf:
        - $ref: "#/components/schemas/SubscriberUpdateConfig"
      additionalProperties: true
      properties:
        tagId:
          type: string
          description: action_add_tag / action_remove_tag. Tag ID; tag actions resolve it by ID only, so pass a name in tagName instead. has_tag and does_not_have_tag also accept a tag name here. Missing tag definitions are created automatically.
          example: "tag_01HZY8Q2N4"
        tagName:
          type: string
          description: action_add_tag / action_remove_tag / has_tag / does_not_have_tag. Tag name; use instead of tagId when you only know the name.
          example: "newsletter-confirmed"
        listId:
          type: string
          description: action_add_to_list / action_remove_from_list / in_list. Must match an existing list in this company; listName is filled in for you.
        listName:
          type: string
          description: Optional cached list display name for list actions.
        eventName:
          type: string
          description: logic_wait_for_event and event_received conditions. Event to wait for or check.
          example: "email.replied"
        timeoutDays:
          type: number
          description: logic_wait_for_event. Maximum wait in whole days from 1 to 365. Defaults to 7.
        timeoutAction:
          type: string
          enum: [continue, exit]
          description: logic_wait_for_event. Continue to the next node or exit the sequence on timeout. Defaults to continue.
        conditionType:
          type: string
          enum:
            - has_tag
            - does_not_have_tag
            - in_list
            - in_segment
            - event_received
            - link_clicked
            - field_equals
            - field_contains
            - field_greater_than
            - field_less_than
            - has_phone
            - sms_subscribed
          description: logic_condition. Condition evaluated before continuing. has_phone and sms_subscribed need no resource fields.
        segmentId:
          type: string
          description: logic_condition. Segment ID for in_segment.
        segmentName:
          type: string
          description: Optional cached segment display name.
        linkUrl:
          type: string
          description: logic_condition. Optional URL substring for link_clicked. Omit to match any tracked click.
        activityScope:
          type: string
          enum: [ever, this_sequence, previous_email]
          description: logic_condition. Scope for event_received and link_clicked checks.
        fieldName:
          type: string
          description: logic_condition. Subscriber attribute name/path for field comparisons.
        fieldValue:
          type: string
          description: logic_condition. Comparison value for field comparisons.
        url:
          type: string
          description: action_webhook. Destination HTTPS URL called when a subscriber reaches this step. Supports merge tags like {{email}} and {{event.order_id}} resolved at execution time.
        method:
          type: string
          enum: [POST, GET, PUT, PATCH, DELETE]
          description: action_webhook. HTTP method. Defaults to POST.
        headers:
          type: object
          description: action_webhook. Optional string-valued request headers. Values support merge tags. Secret values are redacted on sequence reads.
          additionalProperties:
            type: string
        body:
          type: string
          description: action_webhook. Optional JSON body template for POST/PUT/PATCH requests. Must be valid JSON as written, with merge tags inside quoted string values; a tag in a bare value position is rejected. Tags are resolved at execution time. When omitted, a default payload with the subscriber and sequence context is sent.
        resultKey:
          type: string
          description: action_webhook / action_ai. Where the result is saved for the enrollment. Later steps reference it via {{webhooks.KEY.data.field}} (webhook) or {{ai.KEY.field}} (AI) merge tags. Required for action_ai. Must start with a letter and use only letters, numbers, or underscores (max 64 chars).
        onError:
          type: string
          enum: [continue, exit, fail]
          description: action_webhook / action_ai. Behavior when the step fails - continue to the next step, exit the sequence, or fail the enrollment. Defaults to fail for webhooks and continue for AI steps (fallbacks fill the output fields).
        prompt:
          type: string
          description: action_ai. Prompt template sent to the model, resolved per contact at execution time. Supports merge tags like {{first_name}}, {{event.plan}}, and {{webhooks.KEY.data.field}}. Max 8000 characters.
        outputFields:
          type: array
          description: action_ai. Named values the model must return (1-10). Each key becomes a {{ai.KEY.<key>}} merge tag for later steps. fallback is used when generation fails so emails still send with sensible copy. Combined field maxLength values must fit the step's conservative 2000-token multilingual response budget plus JSON overhead.
          items:
            type: object
            required: [key]
            additionalProperties: false
            properties:
              key:
                type: string
                description: Field key, e.g. subject_line. Must start with a letter and use only letters, numbers, or underscores (max 64 chars); unique within the step.
              description:
                type: string
                description: What the model should produce for this field. Max 300 characters.
              maxLength:
                type: integer
                minimum: 1
                maximum: 4000
                description: Hard cap on stored characters (1-4000). Defaults to 500. Output beyond it is cut.
              fallback:
                type: string
                description: Text used verbatim when generation fails or the model omits the field.
        includeTags:
          type: boolean
          description: action_ai. Include the contact's tags in the prompt context.
        includeEventProperties:
          type: boolean
          description: action_ai. Include the enrollment's trigger event name and properties in the prompt context.
        includeRecentEvents:
          type: boolean
          description: action_ai. Include the contact's most recent custom events (newest first) in the prompt context.
        recentEventLimit:
          type: integer
          minimum: 1
          maximum: 50
          description: action_ai. How many recent events to include when includeRecentEvents is true. Defaults to 10.
        includeAttributes:
          type: array
          items:
            type: string
          description: action_ai. Custom attribute keys to include in the prompt context (max 30). Only the listed keys are sent.
        delayDays:
          type: number
          description: logic_delay. Whole days to wait.
        delayHours:
          type: number
          description: logic_delay. Hours to wait.
        delayMinutes:
          type: number
          description: logic_delay. Minutes to wait.

    SequenceSubscriberUpdateStepUpdateInput:
      type: object
      required: [nodeId, config]
      properties:
        nodeId:
          type: string
          description: Target action_update_attributes node ID.
        config:
          $ref: "#/components/schemas/SubscriberUpdateConfig"

    SequenceSmsStepUpdateInput:
      type: object
      required:
        - nodeId
      properties:
        nodeId:
          type: string
          description: Target action_sms node ID from GET /sequences/{sequenceId}.
          example: "node_sms123"
        text:
          type: string
          description: Replacement plain-text message body. Merge tags like {{FIRST_NAME}} work. Provide text or blocks, not both.
          example: "Hey {{FIRST_NAME}}, your order shipped!"
        blocks:
          type: array
          description: Replacement SMS content blocks (text + image subset).
          items:
            type: object
        imageUrls:
          type: array
          description: Up to 2 publicly reachable MMS image URLs. Only valid together with text.
          items:
            type: string
        label:
          type: string
          description: Updated display label for the step.
          example: "Shipping text"
        ineligibleAction:
          type: string
          enum:
            - skip
            - exit
          description: Updated behavior when the contact can't receive SMS.

    SequencePosition:
      type: object
      properties:
        x:
          type: number
          example: 300
        y:
          type: number
          example: 150

    SequenceNode:
      type: object
      properties:
        id:
          type: string
          example: "node_abc123"
        automationId:
          type: string
          example: "seq_abc123"
        nodeType:
          type: string
          example: "action_email"
        config:
          type: object
          additionalProperties: true
        structuralStepNumber:
          type: number
          description: Graph-derived email depth. Present on action_email and action_ab_test nodes reachable from a trigger; parallel branch emails intentionally share a depth.
          example: 1
        updatedAt:
          type: string
          format: date-time
          description: Node concurrency timestamp. Return this as expectedUpdatedAt when patching the node.
          example: "2026-07-17T10:00:00.000Z"
        updateHints:
          type: object
          description: Type-aware guidance for safely patching this node.
          properties:
            tool:
              type: string
              example: "update_sequence_node"
            editableFields:
              type: array
              items:
                type: string
            managedFields:
              type: array
              items:
                type: string
            notes:
              type: array
              items:
                type: string
            expectedUpdatedAt:
              type: string
              format: date-time
              description: Ready-to-return optimistic-concurrency token for update_sequence_node or update_sequence_nodes.
              example: "2026-07-17T10:00:00.000Z"
        position:
          $ref: "#/components/schemas/SequencePosition"

    SequenceAbTestStepVariant:
      type: object
      properties:
        variantId:
          type: string
          example: "var_b"
        variantLabel:
          type: string
          example: "B"
        emailId:
          type: string
          description: Email template holding this variant's stored copy.
          example: "email_variant_b"
        subject:
          type: string
          nullable: true
          example: "Your pick is waiting"
        previewText:
          type: string
          nullable: true
        isWinner:
          type: boolean
        blockCount:
          type: number
          description: Number of blocks in this variant's body. Read the blocks themselves with GET /ab-tests/{abTestId}.
          example: 4

    AbTestContentEditing:
      type: object
      description: Where a sequence A/B step's copy lives and which tools change it.
      required:
        - readTool
        - writeTool
        - requiredScopes
        - note
      properties:
        readTool:
          type: string
          example: "get_ab_test"
        writeTool:
          type: string
          example: "update_ab_test_variant"
        requiredScopes:
          type: array
          items:
            type: string
          example: ["ab_tests:read", "ab_tests:write", "sequences:write"]
        note:
          type: string

    SequenceAbTestStepSummary:
      type: object
      description: A/B test attached to an action_ab_test step. The step's own subject, previewText, and blocks are control variant A only. Without ab_tests:read, record-backed fields are null and variants is empty while the configured id and editing guidance remain available.
      properties:
        id:
          type: string
          nullable: true
          example: "ab_abc123"
        name:
          type: string
          nullable: true
        status:
          type: string
          nullable: true
          example: "testing"
        testType:
          type: string
          nullable: true
          enum:
            - subject
            - content
        winnerCriteria:
          type: string
          nullable: true
          example: "click_rate"
        winnerThreshold:
          type: number
          nullable: true
        winningVariantId:
          type: string
          nullable: true
        variantCount:
          type: number
          example: 2
        variants:
          type: array
          items:
            $ref: "#/components/schemas/SequenceAbTestStepVariant"
        contentEditing:
          $ref: "#/components/schemas/AbTestContentEditing"

    SequenceEmail:
      type: object
      properties:
        nodeId:
          type: string
          example: "node_abc123"
        nodeType:
          type: string
          enum:
            - action_email
            - action_ab_test
          description: Step type. action_ab_test steps keep their copy on the A/B test variants; the fields below report control variant A.
          example: action_email
        abTest:
          nullable: true
          description: Null on ordinary email steps. Present on action_ab_test steps.
          allOf:
            - $ref: "#/components/schemas/SequenceAbTestStepSummary"
        emailId:
          type: string
          nullable: true
          example: "email_abc123"
        stepNumber:
          type: number
          nullable: true
          example: 1
        structuralStepNumber:
          type: number
          nullable: true
          description: Graph-derived email depth, or null when the node is not reachable from a trigger. Parallel branch emails intentionally share a depth.
          example: 1
        name:
          type: string
          nullable: true
          example: "Welcome Sequence - Email 1"
        subject:
          type: string
          nullable: true
          example: "Welcome to Acme"
        previewText:
          type: string
          nullable: true
          example: "Start here"
        isTransactional:
          type: boolean
        senderProfileId:
          type: string
          nullable: true
        replyProfileId:
          type: string
          nullable: true
        fromName:
          type: string
          nullable: true
        replyTo:
          type: string
          nullable: true
        ccEmails:
          type: array
          nullable: true
          items:
            type: string
            format: email
        bccEmails:
          type: array
          nullable: true
          items:
            type: string
            format: email
        attachments:
          type: array
          items:
            $ref: "#/components/schemas/UrlAttachment"
          description: URL-backed file attachments configured on this email step, including event-backed URL templates.
        emailPreset:
          type: string
          nullable: true
          enum:
            - branded
            - minimal
          description: Effective per-email Style > Format derived from native persisted blocks, including emails that contain supported custom HTML blocks. Null when the node has no linked email or the entire email is standalone raw HTML.
          example: minimal
        emailTheme:
          type: object
          nullable: true
          additionalProperties: true
          description: Theme override stored on this step's linked email. Null when the step has no override and renders on the company theme.
        delayNodeId:
          type: string
          nullable: true
          description: ID of the logic_delay node immediately before this email, when present.
          example: "node_delay_abc123"
        delayMs:
          type: number
          nullable: true
          description: Delay before this email in milliseconds, derived from the preceding logic_delay node when present.
          example: 259200000
        delayMode:
          type: string
          nullable: true
          enum:
            - duration
            - until_date
            - until_weekday
          description: Delay mode for the preceding logic_delay node, when present.
          example: "until_date"
        waitUntil:
          allOf:
            - $ref: "#/components/schemas/SequenceWaitUntilInput"
          nullable: true
          description: Date-field wait metadata for dynamic wait-until-date delays.
        waitUntilWeekday:
          allOf:
            - $ref: "#/components/schemas/SequenceWaitUntilWeekdayInput"
          nullable: true
          description: Weekday-window metadata for dynamic wait-until-weekday delays.
        delayDisplay:
          type: string
          nullable: true
          description: Human-readable delay before this email, derived from the preceding logic_delay node when present.
          example: "3 days"
        delayDescription:
          type: string
          nullable: true
          description: Description stored on the preceding logic_delay node, when present.
          example: "Wait 3 days"
        blocks:
          type: array
          items:
            $ref: "#/components/schemas/EmailBlock"

    SequenceSummary:
      type: object
      properties:
        id:
          type: string
          example: "seq_abc123"
        name:
          type: string
          example: "Welcome Sequence"
        description:
          type: string
          nullable: true
        userCancellable:
          type: boolean
        labels:
          type: array
          items:
            type: string
        labelIds:
          type: array
          items:
            type: string
        status:
          $ref: "#/components/schemas/SequenceStatus"
        enrollmentPaused:
          type: boolean
          description: Whether new enrollments are paused while current recipients continue.
        effectiveStatus:
          $ref: "#/components/schemas/SequenceEffectiveStatus"
        acceptsNewEnrollments:
          type: boolean
          description: Whether new subscribers can enter the sequence right now.
        processesExistingEnrollments:
          type: boolean
          description: Whether subscribers already inside the sequence keep advancing and receiving steps.
        effectiveStatusSummary:
          type: string
          description: One plain-language sentence describing the run state, safe to show a user verbatim.
        pauseReason:
          type: string
          nullable: true
        pauseSource:
          type: string
          nullable: true
        pausedByUserId:
          type: string
          nullable: true
        pausedByUser:
          type: object
          nullable: true
          properties:
            id:
              type: string
            name:
              type: string
            email:
              type: string
              format: email
        pausedAt:
          type: string
          format: date-time
          nullable: true
        trigger:
          type: string
          nullable: true
          example: "trigger_list"
        triggerConfig:
          type: object
          nullable: true
          additionalProperties: true
        sendingWindow:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SequenceSendingWindow"
        bccEmails:
          type: array
          nullable: true
          items:
            type: string
          description: Email addresses blind-copied on every email this sequence sends.
          example: ["cs-team@example.com"]
        senderProfileId:
          type: string
          nullable: true
        fromName:
          type: string
          nullable: true
        fromEmail:
          type: string
          format: email
          nullable: true
        replyProfileId:
          type: string
          nullable: true
        replyToName:
          type: string
          nullable: true
        replyToEmail:
          type: string
          format: email
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SequenceDetails:
      allOf:
        - $ref: "#/components/schemas/SequenceSummary"
        - type: object
          properties:
            nodes:
              type: array
              items:
                $ref: "#/components/schemas/SequenceNode"
            edges:
              type: array
              description: Editable sequence topology. Pass the complete set back to graphEdit.edges when replacing edges.
              items:
                $ref: "#/components/schemas/SequenceGraphEdgeInput"
            graphRevision:
              type: string
              description: Revision token for optimistic graph edits. Supply this as graphEdit.expectedRevision.
              example: "75afbbfe05c1a96dc4f167eecc80b14113a3b76b47fb1b193492ddcd2b1d3e21"
            emails:
              type: array
              items:
                $ref: "#/components/schemas/SequenceEmail"
            stopCondition:
              $ref: "#/components/schemas/SequenceStopCondition"
            enrichmentStatus:
              type: string
              enum: [pending, in_progress, complete]
              example: "complete"
            emailCount:
              type: number
              description: Number of email-sending steps, including action_ab_test steps.
              example: 2
            abTestCount:
              type: number
              description: How many of the email steps are A/B test steps.
              example: 1
            discountCount:
              type: number
              example: 1
            subscriberUpdateCount:
              type: number
              example: 1
            enrichedCount:
              type: number
              example: 2

    SequenceCreateResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: "Sequence created with 2 emails and 1 discount action. Use POST /api/v1/sequences/{id}/enable to activate."
        sequence:
          type: object
          properties:
            id:
              type: string
              example: "seq_abc123"
            name:
              type: string
              example: "Welcome Sequence"
            status:
              $ref: "#/components/schemas/SequenceStatus"
            enrollmentPaused:
              type: boolean
              description: Whether new enrollments are paused while current recipients continue.
            effectiveStatus:
              $ref: "#/components/schemas/SequenceEffectiveStatus"
            acceptsNewEnrollments:
              type: boolean
              description: Whether new subscribers can enter the sequence right now.
            processesExistingEnrollments:
              type: boolean
              description: Whether subscribers already inside the sequence keep advancing and receiving steps.
            effectiveStatusSummary:
              type: string
              description: One plain-language sentence describing the run state, safe to show a user verbatim.
            trigger:
              type: string
              example: "trigger_list"
            emailCount:
              type: number
              example: 2
            discountCount:
              type: number
              example: 1
            subscriberUpdateCount:
              type: number
              example: 1
            nodeCount:
              type: number
              example: 5
            enrichmentStatus:
              type: string
              example: "processing"
            stopCondition:
              $ref: "#/components/schemas/SequenceStopCondition"
            sendingWindow:
              nullable: true
              allOf:
                - $ref: "#/components/schemas/SequenceSendingWindow"
        warnings:
          $ref: "#/components/schemas/BlockFieldWarnings"
        eventTrackingCode:
          type: string
          description: Code snippet returned for custom event triggers.
        eventTracking:
          type: object
          description: Endpoint, payload contract, example, documentation, and integration-guide pointer returned for custom event triggers.
          properties:
            endpoint:
              type: string
              example: "https://api.sequenzy.com/api/v1/subscribers/events"
            method:
              type: string
              example: "POST"
            docsUrl:
              type: string
              example: "https://docs.sequenzy.com/api-reference/subscribers/events/trigger"
            integrationGuide:
              type: object
              properties:
                tool:
                  type: string
                  example: "get_integration_guide"
                arguments:
                  type: object
                  additionalProperties: true
                  example: { "use_case": "event_tracking" }
            payloadContract:
              type: object
              properties:
                required:
                  type: array
                  items:
                    type: string
                identity:
                  type: string
                event:
                  type: string
                properties:
                  type: string
                requiredPropertyPaths:
                  type: array
                  items:
                    type: string
                propertyFilters:
                  type: array
                  description: Normalized trigger filters that the event properties must satisfy before the sequence can enroll the subscriber.
                  items:
                    $ref: "#/components/schemas/SequenceTriggerPropertyFilter"
            examplePayload:
              type: object
              additionalProperties: true
              example:
                {
                  "email": "user@example.com",
                  "event": "trial.started",
                  "properties": { "trial_id": "<trial_id>" },
                }
            examplePayloadMatchesFilters:
              type: boolean
              description: Whether examplePayload already satisfies every normalized property filter. When false, adapt properties using payloadContract before sending the sample.
              example: true
            examplePayloadNote:
              type: string
              description: Present when the generated example needs manual property adaptation before it satisfies every filter.
        requiredEvents:
          type: array
          items:
            type: string

    SequenceActionResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: "Sequence enabled"
        sequenceId:
          type: string
          example: "seq_abc123"
        status:
          $ref: "#/components/schemas/SequenceStatus"
        enrollmentPaused:
          type: boolean
          description: Present for enable/disable and enrollment pause/resume actions.
          example: true
        effectiveStatus:
          $ref: "#/components/schemas/SequenceEffectiveStatus"
        acceptsNewEnrollments:
          type: boolean
          description: Whether new subscribers can enter the sequence right now.
        processesExistingEnrollments:
          type: boolean
          description: Whether subscribers already inside the sequence keep advancing and receiving steps.
        effectiveStatusSummary:
          type: string
          description: One plain-language sentence describing the run state, safe to show a user verbatim.

    SequenceGoal:
      type: object
      properties:
        id:
          type: string
        automationId:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        triggerType:
          type: string
          enum: [event, attribute_change]
        triggerEventName:
          type: string
        attributePath:
          type: string
          nullable: true
        attributeCondition:
          type: string
          nullable: true
          enum: [changed, changed_to, changed_from_to]
        attributeValue:
          type: string
          nullable: true
        attributePreviousValue:
          type: string
          nullable: true
        eventPropertyName:
          type: string
          nullable: true
        eventPropertyLabel:
          type: string
          nullable: true
        attributionWindowHours:
          type: integer
        isActive:
          type: boolean

    SequenceGoalInput:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          nullable: true
        triggerType:
          type: string
          enum: [event, attribute_change]
        triggerEventName:
          type: string
          nullable: true
        attributePath:
          type: string
          nullable: true
        attributeCondition:
          type: string
          nullable: true
          enum: [changed, changed_to, changed_from_to]
        attributeValue:
          type: string
          nullable: true
        attributePreviousValue:
          type: string
          nullable: true
        eventPropertyName:
          type: string
          nullable: true
        eventPropertyLabel:
          type: string
          nullable: true
        attributionWindowHours:
          type: integer
          minimum: 1
          maximum: 720
        isActive:
          type: boolean

    SequenceInboundWebhook:
      type: object
      properties:
        id:
          type: string
        secret:
          type: string
          description: Secret URL token. Treat it as a credential.
        url:
          type: string
          format: uri
        eventName:
          type: string
        fieldMapping:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/SequenceInboundWebhookFieldMapping"
        samplePayload:
          type: object
          nullable: true
          additionalProperties: true
        sampleCapturedAt:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum: [pending_setup, active]
        createdAt:
          type: string
          format: date-time

    SequenceInboundWebhookFieldMapping:
      type: object
      required: [email]
      properties:
        email:
          type: string
          description: Dot path to the subscriber email in the webhook payload.
        firstName:
          type: string
        lastName:
          type: string
        properties:
          type: object
          additionalProperties:
            type: string

    BulkSubscriberTagRequest:
      type: object
      required:
        - tags
      description: "Provide tags plus at least one identifier list. Identifier lists may be combined and total at most 500 entries per request."
      properties:
        tags:
          type: array
          minItems: 1
          maxItems: 25
          items:
            type: string
            minLength: 1
          description: Tag names applied to every matched subscriber. Names are normalized the same way as single-subscriber tag endpoints.
          example: ["derived-churn-risk"]
        emails:
          type: array
          items:
            type: string
            format: email
          description: Subscriber emails to update.
        externalIds:
          type: array
          items:
            type: string
          description: Customer-owned subscriber IDs to update.
        subscriberIds:
          type: array
          items:
            type: string
          description: Sequenzy subscriber IDs to update.
        triggerAutomations:
          type: boolean
          description: Add only. Whether tag_added sequences may enroll these contacts. Defaults to false and requires the automations:trigger scope.
          example: false

    BulkSubscriberTagResponse:
      type: object
      properties:
        success:
          type: boolean
          description: False when any matched subscriber failed to update.
          example: true
        tags:
          type: array
          items:
            type: string
        requested:
          type: integer
          description: Identifiers supplied in the request.
          example: 3
        matched:
          type: integer
          description: Existing subscribers resolved from those identifiers.
          example: 2
        updated:
          type: integer
          description: Subscribers whose tags actually changed.
          example: 2
        unchanged:
          type: integer
          description: Subscribers that were already in the target state.
          example: 0
        failed:
          type: integer
          example: 0
        notFound:
          type: object
          description: Identifiers that did not resolve. These subscribers were not created.
          properties:
            emails:
              type: array
              items:
                type: string
            externalIds:
              type: array
              items:
                type: string
            subscriberIds:
              type: array
              items:
                type: string
        failures:
          type: array
          description: Up to 50 per-subscriber failures.
          items:
            type: object
            properties:
              subscriberId:
                type: string
              email:
                type: string
                nullable: true
              error:
                type: string
        triggeredAutomations:
          type: boolean
          description: Add only. Whether tag automations were allowed to run.
        message:
          type: string
          example: "Tagged 2 subscribers (0 already in the target state, 0 failed)."

    SequenceEnrollmentCancelRequest:
      type: object
      description: "Provide exactly one target: cancelAll, subscriberId, subscriberIds, or fieldValues."
      properties:
        cancelAll:
          type: boolean
          description: Cancel every active or waiting enrollment in the sequence, regardless of how contacts entered it. Use this when segment-triggered enrollments share no entry field value. Defaults to dry run unless dryRun is explicitly false.
          example: true
        subscriberId:
          type: string
          description: Subscriber ID to cancel in this sequence.
          example: "sub_abc123"
        subscriberIds:
          type: array
          description: Up to 500 subscriber IDs to cancel in this sequence. IDs that do not resolve are returned in target.notFoundSubscriberIds. Defaults to dry run unless dryRun is explicitly false.
          items:
            type: string
          example: ["sub_abc123", "sub_def456"]
        fieldPath:
          type: string
          description: Dot-path inside the token's stored entry event properties. If omitted, the sequence enrollmentFieldPath is used.
          example: "order.id"
        fieldValues:
          type: array
          description: Entry field values to match.
          items:
            type: string
          example: ["ord_123", "ord_456"]
        dryRun:
          type: boolean
          description: When true, returns matching enrollments without cancelling them. cancelAll, subscriberIds, and fieldValues default to dry run unless explicitly false; a single subscriberId cancels immediately.
          example: false
        reason:
          type: string
          description: Optional reason stored on cancelled enrollment tokens.
          example: "Order cancelled"

    SequenceEnrollmentMoveRequest:
      type: object
      required:
        - fromNodeId
      properties:
        fromNodeId:
          type: string
          description: Node ID the contacts are currently sitting on, such as the delay step they are waiting at.
          example: "node_delay_2"
        targetNodeId:
          type: string
          description: Node ID to move them onto. Defaults to the source step's only next step, and is required when that step branches or is terminal. Cannot be the trigger node.
          example: "node_email_3"
        limit:
          type: number
          description: Maximum enrollments to move in this call. Defaults to 100, maximum 500.
          example: 180
        sort:
          type: string
          enum:
            [wait_until_asc, wait_until_desc, enrolled_at_asc, enrolled_at_desc]
          description: Which enrollments to take first. Defaults to wait_until_asc, the contacts that have been waiting longest for their next step.
          example: "wait_until_asc"
        subscriberIds:
          type: array
          description: Optional narrowing filter. Only move these subscribers, up to 500.
          items:
            type: string
          example: ["sub_abc123", "sub_def456"]
        dailyLimit:
          type: number
          description: Refuses to move more than this many enrollments onto targetNodeId in a rolling 24 hours, counting the moves recorded by earlier calls.
          example: 500
        tags:
          type: array
          description: Existing tag names applied to the moved contacts. Requires the subscribers:tag scope. Applying them never enrolls contacts in tag_added sequences.
          items:
            type: string
          example: ["wave-3-released"]
        reason:
          type: string
          description: Note stored on every moved enrollment and returned as moveReason when listing enrollments.
          example: "Wave 3"
        dryRun:
          type: boolean
          description: When true (the default), reports which enrollments would move without moving them.
          example: false

    SequenceEnrollmentMoveResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        dryRun:
          type: boolean
          example: false
        fromNodeId:
          type: string
          example: "node_delay_2"
        targetNodeId:
          type: string
          example: "node_email_3"
        sort:
          type: string
          example: "wait_until_asc"
        requestedLimit:
          type: number
          example: 180
        effectiveLimit:
          type: number
          description: How many this call was allowed to move after the daily guardrail was applied.
          example: 180
        matchedCount:
          type: number
          description: Movable enrollments parked on fromNodeId when the request started.
          example: 4320
        movedCount:
          type: number
          example: 180
        remainingCount:
          type: number
          description: Movable enrollments still on fromNodeId. Repeat the same request while this is above zero.
          example: 4140
        skippedCount:
          type: number
          description: Enrollments excluded because they are active, a worker is mid-step on them, or they are parked awaiting double opt-in. Only safely parked waiting tokens can be moved.
          example: 0
        dailyLimit:
          type: number
          nullable: true
          example: 500
        movedInWindow:
          type: number
          description: Moves onto targetNodeId already recorded in the rolling 24-hour window.
          example: 0
        dailyRemaining:
          type: number
          nullable: true
          example: 320
        enqueuedCount:
          type: number
          description: Moved enrollments handed to the worker queue. Zero while the sequence is not running.
          example: 180
        enqueueErrors:
          type: array
          items:
            type: object
            properties:
              tokenId:
                type: string
                example: "tok_abc123"
              error:
                type: string
                example: "Redis connection lost"
        tagResult:
          type: object
          nullable: true
          additionalProperties: true
        enrollments:
          type: array
          items:
            type: object
            properties:
              tokenId:
                type: string
                example: "tok_abc123"
              subscriberId:
                type: string
                example: "sub_abc123"
              subscriberEmail:
                type: string
                nullable: true
                example: "customer@example.com"
              status:
                type: string
                example: "waiting"
              enrollmentKey:
                type: string
                example: "__default__"
        hasMore:
          type: boolean
          example: true
        message:
          type: string
          example: "Moved 180 enrollments. 4140 still wait on the source step; repeat this request to release more."

    SequenceEnrollmentCancelResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        dryRun:
          type: boolean
          example: false
        target:
          type: object
          additionalProperties: true
        matchedCount:
          type: number
          description: Active or waiting enrollments matching the target when the request started.
          example: 2
        cancelledCount:
          type: number
          description: Enrollments cancelled by this request. Bulk cancellation is capped at 1000 per request.
          example: 2
        remainingCount:
          type: number
          description: Enrollments still matching the target after this request. Repeat the same request while this is above zero.
          example: 0
        enrollments:
          type: array
          items:
            type: object
            properties:
              tokenId:
                type: string
                example: "tok_abc123"
              subscriberId:
                type: string
                example: "sub_abc123"
              subscriberEmail:
                type: string
                nullable: true
                example: "customer@example.com"
              status:
                type: string
                example: "cancelled"
              enrollmentKey:
                type: string
                example: "event:ecommerce.order_placed:field:order.id:ord_123"
        hasMore:
          type: boolean
          example: false
        message:
          type: string
          example: "Cancelled 2 sequence enrollments."

    SequenceEnrollmentRealignRequest:
      type: object
      description: All fields are optional. With no body, every waiting enrollment in the sequence is previewed.
      properties:
        nodeIds:
          type: array
          description: Step IDs to limit realignment to. Defaults to every step.
          minItems: 1
          items:
            type: string
          example: ["node_week_14"]
        subscriberIds:
          type: array
          description: Up to 500 subscriber IDs to limit realignment to. Defaults to every waiting contact.
          minItems: 1
          maxItems: 500
          items:
            type: string
          example: ["sub_abc123", "sub_def456"]
        cursor:
          type: string
          description: Opaque continuation cursor. When a response has hasMore true, pass its nextCursor here to continue after the enrollments already scanned.
        dryRun:
          type: boolean
          description: When true (the default), returns the new wait times without writing them. Set false to apply.
          example: false

    SequenceEnrollmentRealignResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        sequenceName:
          type: string
          example: "52-Week Medicine Wheel"
        dryRun:
          type: boolean
          example: false
        sendingWindow:
          type: object
          nullable: true
          description: The sequence sending window realignment anchored on, or null when only per-step weekday gates applied.
          additionalProperties: true
        scannedCount:
          type: number
          description: Waiting enrollments inspected by this request.
          example: 330
        realignedCount:
          type: number
          description: Waiting enrollments moved earlier, or that would move on a dry run. Capped at 1000 per request.
          example: 330
        unchangedCount:
          type: number
          example: 0
        unchangedReasons:
          type: object
          description: "Counts per reason an enrollment did not move: already_at_window_start, already_due, day_not_allowed, no_shared_opening, no_window, not_email_bound, send_retry, raced."
          additionalProperties:
            type: number
        requeueFailedCount:
          type: number
          description: Enrollments whose new wait time was stored but whose wake-up could not be re-queued. The stuck-enrollment sweeper recovers these within a few minutes.
          example: 0
        changes:
          type: array
          description: Sample of up to 50 realigned enrollments.
          items:
            type: object
            properties:
              enrollmentId:
                type: string
                example: "tok_abc123"
              subscriberId:
                type: string
                example: "sub_abc123"
              subscriberEmail:
                type: string
                nullable: true
                example: "customer@example.com"
              currentNodeId:
                type: string
                example: "node_week_14"
              waitUntil:
                type: string
                format: date-time
                example: "2026-08-16T18:13:00.000Z"
              newWaitUntil:
                type: string
                format: date-time
                example: "2026-08-16T15:00:00.000Z"
              movedEarlierMinutes:
                type: number
                example: 193
        hasMore:
          type: boolean
          description: True when a per-request cap stopped the scan early. Continue with nextCursor while this is true.
          example: false
        nextCursor:
          type: string
          description: Opaque continuation cursor to pass as cursor on the next request when hasMore is true.
        maxRealignmentsPerRequest:
          type: number
          example: 1000
        message:
          type: string
          example: "Realigned 330 waiting enrollments to the sending-window opening on the day they were already scheduled for."

    SequenceEnrollmentRealignQueuedResponse:
      type: object
      required: [success, sequenceId, dryRun, status, jobId, message]
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        sequenceName:
          type: string
          example: "52-Week Medicine Wheel"
        dryRun:
          type: boolean
          enum: [false]
        status:
          type: string
          enum: [queued]
        jobId:
          type: string
          example: "realign-sequence-enrollments-seq_abc123-550e8400-e29b-41d4-a716-446655440000"
        message:
          type: string

    SequenceEnrollmentRealignJobResponse:
      type: object
      required:
        [success, sequenceId, dryRun, status, jobId, result, error, message]
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        sequenceName:
          type: string
          example: "52-Week Medicine Wheel"
        dryRun:
          type: boolean
          enum: [false]
        status:
          type: string
          enum: [queued, running, completed, failed]
        jobId:
          type: string
        result:
          type: object
          nullable: true
          description: Completed realignment result; null while queued, running, or failed.
          additionalProperties: true
        error:
          type: string
          nullable: true
        message:
          type: string

    SequenceEnrollmentListResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        sequenceId:
          type: string
          example: "seq_abc123"
        sequenceName:
          type: string
          example: "Onboarding"
        statuses:
          type: array
          description: Enrollment statuses included in this response.
          items:
            type: string
          example: ["active", "waiting"]
        stopCondition:
          $ref: "#/components/schemas/SequenceStopCondition"
          description: The sequence's single configured stop condition, including any matchConfig event-property filters or field comparison. A sequence holds exactly one, so a stop event with any other name never applies. It is re-evaluated when an enrollment next runs a step, not when its event arrives.
        stopConditionMatchEvaluatedCount:
          type: integer
          description: How many enrollments in this page were actually evaluated for a current stop-condition match. 0 when stopConditionMatch was not requested, the sequence has no stop condition, or no returned enrollment was still active or waiting.
          example: 1
        enrollments:
          type: array
          items:
            type: object
            properties:
              enrollmentId:
                type: string
                description: Enrollment token ID. Stable identifier for this one run through the sequence.
                example: "tok_abc123"
              sequenceId:
                type: string
                example: "seq_abc123"
              subscriberId:
                type: string
                example: "sub_abc123"
              email:
                type: string
                nullable: true
                description: Subscriber email address. Falls back to the address captured at enrollment when the subscriber record no longer exists.
                example: "customer@example.com"
              firstName:
                type: string
                nullable: true
                example: "Wanda"
              lastName:
                type: string
                nullable: true
                example: "Waiter"
              subscriberStatus:
                type: string
                nullable: true
                example: "active"
              status:
                type: string
                enum:
                  - active
                  - waiting
                  - completed
                  - failed
                  - cancelled
                example: "waiting"
              currentNodeId:
                type: string
                description: Sequence node this enrollment is currently sitting on.
                example: "node_wave_1"
              currentNodeType:
                type: string
                description: Omitted when the node no longer exists in the sequence graph.
                example: "logic_delay"
              currentNodeLabel:
                type: string
                description: Node label or email subject when available.
                example: "Start at Wave 1"
              currentNodeMissing:
                type: boolean
                example: false
              enrollmentKey:
                type: string
                example: "__default__"
              enrollmentStartedAt:
                type: string
                format: date-time
                example: "2026-01-01T00:00:00.000Z"
              waitUntil:
                type: string
                format: date-time
                nullable: true
                description: When a waiting enrollment is scheduled to resume, or null when nothing is scheduled.
                example: "2026-01-04T00:00:00.000Z"
              lastUpdatedAt:
                type: string
                format: date-time
                description: Last change to this enrollment. For a waiting enrollment this is when it arrived at its current node.
                example: "2026-01-01T00:00:00.000Z"
              failedReason:
                type: string
                nullable: true
                description: Why this enrollment stopped, for status `failed`. Null for every other status and for failures recorded before this field existed. The same reason repeated across enrollments on one `currentNodeId` points at that step rather than at the contacts.
                example: "Automation seq_abc123 not found"
              movedFromNodeId:
                type: string
                nullable: true
                description: Step this enrollment was released from by POST /sequences/{sequenceId}/enrollments/move, or null when it reached its current step on its own.
                example: "node_delay_2"
              movedAt:
                type: string
                format: date-time
                nullable: true
                description: When that release happened, or null when the enrollment was never moved.
                example: "2026-03-01T09:00:00.000Z"
              moveReason:
                type: string
                nullable: true
                description: Note recorded with that release, or null when none was given.
                example: "Wave 3"
              stopConditionMatches:
                type: boolean
                nullable: true
                description: Whether the sequence stop condition matches for this contact right now. Null when it was not determined - stopConditionMatch was not requested, the sequence has no stop condition, this enrollment is no longer active or waiting, or it fell outside the evaluated window. Null never means "does not match". This is a non-atomic snapshot; the worker re-checks before a future step, but the condition can change and a step already past its stop check may still finish.
                example: true
              stopConditionMatchReason:
                type: string
                nullable: true
                description: Human-readable reason the stop condition matches. Null when stopConditionMatches is not true.
                example: "Subscriber received stop event: onboarding.completed"
              enteredVia:
                type: object
                description: >-
                  What put this contact into the sequence. The distinguishing
                  detail when a trigger covers several lists or tags.
                properties:
                  kind:
                    type: string
                    enum:
                      [
                        list,
                        tag,
                        segment,
                        event,
                        inactivity,
                        frequency,
                        manual,
                        test_run,
                        unknown,
                      ]
                    description: >-
                      `inactivity` and `frequency` identify time-based
                      evaluation of the monitored event rather than an
                      ordinary event-received enrollment.
                      `manual` is a dashboard or API enrollment that bypassed
                      the trigger. `unknown` covers enrollments recorded before
                      this field existed.
                    example: "list"
                  value:
                    type: string
                    nullable: true
                    description: The list ID, tag name, segment ID, event name, or monitored event name. Null for `manual`, `test_run`, and `unknown`.
                    example: "list_abc123"
                  name:
                    type: string
                    nullable: true
                    description: >-
                      Resolved list or segment name. Set for list and segment
                      kinds, and null when the referenced resource has since
                      been deleted.
                    example: "Newsletter"
                  description:
                    type: string
                    description: Ready-to-display attribution line.
                    example: 'Added to list "Newsletter"'
        pagination:
          type: object
          properties:
            limit:
              type: number
              example: 50
            offset:
              type: number
              example: 0
            count:
              type: number
              example: 1
            total:
              type: number
              example: 1
            hasMore:
              type: boolean
              example: false

    OutboundWebhookEventType:
      type: string
      enum:
        - email.sent
        - email.delivered
        - email.delivery_delayed
        - email.bounced
        - email.failed
        - email.complained
        - email.opened
        - email.clicked
        - email.replied
        - email.unsubscribed
        - sms.sent
        - sms.delivered
        - sms.failed
        - sms.opted_out
        - subscriber.invalid
        - subscriber.created
        - subscriber.updated
        - subscriber.unsubscribed
        - subscriber.list_subscribed
        - subscriber.list_unsubscribed
        - sequence.finished
        - sequence.failed
        - poll.answered

    NotificationPreference:
      type: object
      required:
        - event
        - mode
      properties:
        event:
          type: string
          enum:
            - new_subscriber
            - campaign_completed
          description: Which notification to configure.
        mode:
          type: string
          enum:
            - "off"
            - instant
            - daily
          description: How to receive it. "instant" sends one email per occurrence, "daily" one summary per day. "daily" is not supported for campaign_completed.

    NotificationPreferences:
      type: object
      properties:
        success:
          type: boolean
        notificationPreferences:
          type: array
          description: Every notification event with its current mode, defaults included.
          items:
            $ref: "#/components/schemas/NotificationPreference"
        supportedModes:
          type: object
          description: Modes each event accepts, keyed by event.
          additionalProperties:
            type: array
            items:
              type: string
        defaults:
          type: object
          description: Mode each event uses when the user has never configured it.
          additionalProperties:
            type: string

    OutboundWebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          example: "webhook_abc123"
        name:
          type: string
          example: "Production webhook"
        url:
          type: string
          format: uri
          example: "https://example.com/sequenzy/webhooks"
        status:
          type: string
          enum: [enabled, disabled]
        events:
          type: array
          items:
            $ref: "#/components/schemas/OutboundWebhookEventType"
        consecutiveFailures:
          type: integer
          description: Consecutive failed delivery attempts used for endpoint backoff.
        circuitOpenedAt:
          type: string
          format: date-time
          nullable: true
        circuitOpenUntil:
          type: string
          format: date-time
          nullable: true
        signingSecret:
          type: string
          description: Returned only when creating a webhook or adding a signing secret.
          example: "whsec_..."
        signingSecrets:
          type: array
          description: Active signing secret metadata. Secret values are returned only once.
          items:
            type: object
            properties:
              id:
                type: string
                example: "sec_abc123"
              prefix:
                type: string
                example: "whsec_abcd12"
              createdAt:
                type: string
                format: date-time
        lastSuccessAt:
          type: string
          format: date-time
          nullable: true
        lastFailureAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    OutboundWebhookDelivery:
      type: object
      properties:
        id:
          type: string
        eventId:
          type: string
        eventType:
          $ref: "#/components/schemas/OutboundWebhookEventType"
        status:
          type: string
          enum: [pending, delivering, succeeded, failed, skipped]
        attempts:
          type: integer
        queuedAt:
          type: string
          format: date-time
          nullable: true
        lastStatusCode:
          type: integer
          nullable: true
        lastError:
          type: string
          nullable: true
        lastResponseBody:
          type: string
          nullable: true
        nextAttemptAt:
          type: string
          format: date-time
          nullable: true
        lastAttemptAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    OutboundWebhookDeliveryAttempt:
      type: object
      properties:
        id:
          type: string
        attemptNumber:
          type: integer
        status:
          type: string
          enum: [succeeded, failed]
        statusCode:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
        responseBody:
          type: string
          nullable: true
        durationMs:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time

    TeamMember:
      type: object
      properties:
        id:
          type: string
          description: Member or invitation ID. The owner entry uses the literal id "owner".
          example: "member_abc123"
        kind:
          type: string
          enum: [owner, member, invitation]
        userId:
          type: string
          nullable: true
        role:
          type: string
          enum: [owner, admin, viewer, restricted]
        canManageBilling:
          type: boolean
        user:
          type: object
          properties:
            id:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
            email:
              type: string
              format: email
            image:
              type: string
              nullable: true
        status:
          type: string
          enum: [joined, pending, expired]
        createdAt:
          type: string
          format: date-time
          nullable: true
        invitedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        invitedBy:
          type: object
          nullable: true
          properties:
            id:
              type: string
            name:
              type: string
              nullable: true
            email:
              type: string
              format: email

    ConversationSummary:
      type: object
      properties:
        id:
          type: string
          example: "conv_abc123"
        subscriberId:
          type: string
          nullable: true
        subscriberEmail:
          type: string
          format: email
        subscriberName:
          type: string
          nullable: true
        subject:
          type: string
          nullable: true
        status:
          type: string
          enum: [open, closed]
        messageCount:
          type: integer
        lastMessageAt:
          type: string
          format: date-time
        lastMessageBy:
          type: string
          example: "subscriber"
        hasUnread:
          type: boolean
        createdAt:
          type: string
          format: date-time
        context:
          type: object
          properties:
            type:
              type: string
              description: Originating email type (campaign, sequence, transactional, or unknown).
              example: "campaign"
            label:
              type: string
              nullable: true
              description: Campaign or sequence name.

    ConversationMessage:
      type: object
      properties:
        id:
          type: string
          example: "msg_abc123"
        conversationId:
          type: string
        type:
          type: string
          enum: [inbound, outbound, note]
        subject:
          type: string
          nullable: true
        bodyText:
          type: string
          nullable: true
        bodyHtml:
          type: string
          nullable: true
        fromEmail:
          type: string
          nullable: true
        fromName:
          type: string
          nullable: true
        fromUserId:
          type: string
          nullable: true
        attachments:
          type: array
          items:
            type: object
            additionalProperties: true
        isRead:
          type: boolean
        deliveryStatus:
          type: string
          nullable: true
          description: pending, sent, or failed for outbound messages. Null for notes.
        createdAt:
          type: string
          format: date-time

paths:
  # ============================================================================
  # Email Components
  # ============================================================================
  /email-blocks:
    get:
      summary: List email block types
      description: >-
        Lists every block type accepted by the `blocks` array on campaigns,
        sequence email steps, templates, transactional emails, and email
        components, with the required and optional fields of each. Derived from
        the same schemas that validate a write, so it cannot drift from what
        those endpoints accept.
      operationId: listEmailBlockTypes
      tags:
        - Email Blocks
      parameters:
        - name: creatableOnly
          in: query
          required: false
          schema:
            type: string
            enum: ["true"]
          description: Hide structural block types the editor manages for you.
      responses:
        "200":
          description: Block types listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  blockTypes:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailBlockTypeReference"
                  conditionFields:
                    type: array
                    description: >-
                      The per-field table for block conditions, always returned
                      with the list. This is the call made to find out what
                      exists, and the table adds about 9% to it.
                    items:
                      $ref: "#/components/schemas/EmailBlockConditionFieldReference"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /email-blocks/{type}:
    get:
      summary: Get email block type
      description: Returns the full field reference for one block type, with a minimal valid example and authoring notes.
      operationId: getEmailBlockType
      tags:
        - Email Blocks
      parameters:
        - name: type
          in: path
          required: true
          schema:
            type: string
          description: Block type, for example list, steps, text, or hero.
          example: steps
        - name: conditionFields
          in: query
          required: false
          schema:
            type: string
            enum: ["true"]
          description: >-
            Include the per-field condition table in the response. Not needed
            for `conditional-group`, which always carries it. The table is
            several times the size of one block type's reference, so a targeted
            lookup does not carry it unless asked.
      responses:
        "200":
          description: Block type reference
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  blockType:
                    $ref: "#/components/schemas/EmailBlockTypeReference"
                  conditionFields:
                    type: array
                    description: >-
                      The per-field table for block conditions. Returned when
                      `type` is `conditional-group`, or when
                      `conditionFields=true` is passed.
                    items:
                      $ref: "#/components/schemas/EmailBlockConditionFieldReference"
                  conditionFieldsHint:
                    type: string
                    description: >-
                      Returned in place of `conditionFields` when the table was
                      not included, saying that conditions are per-field and how
                      to request the table.
                    example: >-
                      Every block also takes a `conditions` array, and each
                      condition field accepts only its own operators. Request
                      this endpoint with ?conditionFields=true, or GET
                      /email-blocks, for `conditionFields`: the operators, value
                      format, and render-preview support of every condition
                      field.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown block type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /email-components:
    get:
      summary: List email components
      description: Lists reusable email components newest first, including the components pinned as company defaults.
      operationId: listEmailComponents
      tags:
        - Email Components
      parameters:
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [section, footer]
          description: Filter by component type.
        - name: slot
          in: query
          required: false
          schema:
            type: string
            enum: [footer]
          description: Filter by default slot.
        - name: defaultsOnly
          in: query
          required: false
          schema:
            type: string
            enum: ["true"]
          description: Return only components pinned as a company default.
      responses:
        "200":
          description: Components listed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  components:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailComponent"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create email component
      description: Creates a reusable email component from a block list. Component names are unique per company.
      operationId: createEmailComponent
      tags:
        - Email Components
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - blocks
              properties:
                name:
                  type: string
                  example: "Promo banner"
                description:
                  type: string
                  nullable: true
                blocks:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/EmailBlock"
                componentType:
                  type: string
                  enum: [section, footer]
                  description: Defaults to section. Creating a footer component does not pin it as the company default.
      responses:
        "201":
          description: Component created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  component:
                    $ref: "#/components/schemas/EmailComponent"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Component name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /email-components/defaults/{slot}:
    get:
      summary: Get default email component
      description: Returns the component used as the company default for a slot. A 404 means emails fall back to the generated footer.
      operationId: getDefaultEmailComponent
      tags:
        - Email Components
      parameters:
        - name: slot
          in: path
          required: true
          schema:
            type: string
            enum: [footer]
      responses:
        "200":
          description: Default component returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  component:
                    $ref: "#/components/schemas/EmailComponent"
        "400":
          description: Unsupported slot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No default component set for this slot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      summary: Set default email component
      description: >-
        Creates or replaces the company default component for a slot. New sequence, campaign, and
        AI-generated emails clone this component when they are built. A default footer always keeps
        its unsubscribe link enabled; transactional sends hide it at render time. Emails that already
        exist keep the footer they were built with.
      operationId: setDefaultEmailComponent
      tags:
        - Email Components
      parameters:
        - name: slot
          in: path
          required: true
          schema:
            type: string
            enum: [footer]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - blocks
              properties:
                blocks:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/EmailBlock"
                name:
                  type: string
                  description: Defaults to "Default Footer" when creating the footer default.
                description:
                  type: string
                  nullable: true
      responses:
        "200":
          description: Default component saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  component:
                    $ref: "#/components/schemas/EmailComponent"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Component name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /email-components/{componentId}:
    get:
      summary: Get email component
      description: Returns a single email component by id.
      operationId: getEmailComponent
      tags:
        - Email Components
      parameters:
        - name: componentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Component returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  component:
                    $ref: "#/components/schemas/EmailComponent"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Component not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update email component
      description: >-
        Updates component metadata or replaces its blocks. Replacing blocks bumps the component
        version; emails built earlier keep the copy they were created with. Editing the component
        pinned as the default footer keeps its unsubscribe link enabled.
      operationId: updateEmailComponent
      tags:
        - Email Components
      parameters:
        - name: componentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
                blocks:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/EmailBlock"
                componentType:
                  type: string
                  enum: [section, footer]
      responses:
        "200":
          description: Component updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  component:
                    $ref: "#/components/schemas/EmailComponent"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Component not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Component name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete email component
      description: >-
        Deletes an email component. Emails that already rendered it keep their copied blocks.
        Deleting the pinned default footer makes new emails fall back to the generated footer.
      operationId: deleteEmailComponent
      tags:
        - Email Components
      parameters:
        - name: componentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Component deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  id:
                    type: string
                    example: "cmp_123"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Component not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Email Templates
  # ============================================================================
  /emails:
    post:
      summary: Create email
      description: Creates an email template with block content. Raw HTML is stored as a native HTML body.
      operationId: createEmail
      tags:
        - Emails
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  required:
                    - name
                    - subject
                  properties:
                    name:
                      type: string
                      example: "Welcome email"
                    subject:
                      type: string
                      example: "Welcome"
                    previewText:
                      type: string
                      nullable: true
                      example: "Here's what to do next"
      responses:
        "201":
          description: Email created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  email:
                    $ref: "#/components/schemas/Email"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /emails/{emailId}:
    patch:
      summary: Update email
      description: Updates email metadata or replaces the email body.
      operationId: updateEmail
      tags:
        - Emails
      parameters:
        - name: emailId
          in: path
          required: true
          schema:
            type: string
          description: Email ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  properties:
                    name:
                      type: string
                    subject:
                      type: string
                    previewText:
                      type: string
                      nullable: true
      responses:
        "200":
          description: Email updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  email:
                    $ref: "#/components/schemas/Email"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /emails/{emailId}/blocks:
    patch:
      summary: Update email blocks
      description: Replaces an email body or mutates an existing block type.
      operationId: updateEmailBlocks
      tags:
        - Emails
      parameters:
        - name: emailId
          in: path
          required: true
          schema:
            type: string
          description: Email ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  properties:
                    blockId:
                      type: string
                      description: Existing block ID to mutate.
                    type:
                      type: string
                      description: New block type. Type mutation supports text and html.
                      enum: [text, html]
                    content:
                      type: string
                      description: Optional replacement content for the mutated block.
      responses:
        "200":
          description: Email blocks updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  email:
                    $ref: "#/components/schemas/Email"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Email or block not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Email Sends
  # ============================================================================
  /email-sends:
    get:
      summary: List sent emails
      description: Lists the recent 14-day delivery history with dashboard-equivalent subject, recipient, status, type, bounce, source, pagination, and sorting filters. Successful test sends and copied-recipient bookkeeping rows are hidden; a test send that failed, bounced, or was suppressed IS listed, flagged with an `isTestEmail` value of true, because it is the only record of a test that never arrived.
      operationId: listEmailSends
      tags:
        - Email Sends
      parameters:
        - name: search
          in: query
          schema:
            type: string
          description: Case-insensitive subject/title or recipient search. `q` is accepted as an alias.
        - name: q
          in: query
          deprecated: true
          schema:
            type: string
          description: Compatibility alias for `search`.
        - name: subject
          in: query
          schema:
            type: string
          description: Case-insensitive subject/title filter. `title` is accepted as an alias.
        - name: title
          in: query
          deprecated: true
          schema:
            type: string
          description: Compatibility alias for `subject`.
        - name: recipient
          in: query
          schema:
            type: string
          description: Case-insensitive recipient email filter.
        - name: status
          in: query
          schema:
            type: string
            enum:
              [
                pending,
                sent,
                delivered,
                opened,
                clicked,
                bounced,
                complained,
                failed,
                suppressed,
              ]
          description: Delivery status. Opened includes clicked deliveries.
        - name: emailType
          in: query
          schema:
            type: string
            enum: [campaign, transactional, sequence]
        - name: bounceType
          in: query
          schema:
            type: string
            enum: [Permanent, Transient]
        - name: campaignId
          in: query
          schema:
            type: string
        - name: transactionalEmailId
          in: query
          schema:
            type: string
        - name: automationId
          in: query
          schema:
            type: string
        - name: automationNodeId
          in: query
          schema:
            type: string
          description: Filter to one email step of a sequence. Take the node ID from the `steps` array of the sequence metrics endpoint. Combined with `automationId` the two intersect.
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 14
            default: 14
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: sortField
          in: query
          schema:
            type: string
            enum: [recipientEmail, subject, status, eventAt, sentAt, createdAt]
            default: createdAt
        - name: sortOrder
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        "200":
          description: Paginated sent-email history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  retentionDays:
                    type: integer
                    example: 14
                  emailSends:
                    type: array
                    items:
                      allOf:
                        - $ref: "#/components/schemas/EmailSend"
                        - type: object
                          properties:
                            eventAt:
                              type: string
                              format: date-time
                            opened:
                              type: boolean
                            clicked:
                              type: boolean
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Invalid filter or pagination value
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /email-sends/{emailSendId}:
    get:
      summary: Get email send
      description: Gets an email delivery snapshot by ID, including queued and test sends, the stored HTML body when available, and retained ClickHouse events when the short-lived row has been cleaned up. Test sends remain hidden from sent-email history but are available through this exact-ID endpoint while their row is retained.
      operationId: getEmailSend
      tags:
        - Email Sends
      parameters:
        - name: emailSendId
          in: path
          required: true
          schema:
            type: string
          description: Email send ID
      responses:
        "200":
          description: Email send found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  source:
                    type: string
                    enum: [database, clickhouse_events]
                  message:
                    type: string
                    nullable: true
                  emailSend:
                    $ref: "#/components/schemas/EmailSend"
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailSendEvent"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Email send not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Recipient Suppressions
  # ============================================================================
  /suppressions:
    get:
      summary: List suppressed recipients
      description: |
        Lists the recipients this company cannot reach, newest suppression first by default.

        Four product-level suppression types appear:

        - `suppressionType: invalid_recipient` - SMTP evidence conclusively identifies an invalid destination. It is global, visible to companies associated with the address, and protected.
        - `suppressionType: unknown_hard_bounce` - a permanent/undetermined failure without enough evidence to declare the inbox invalid. It is company-scoped and protected.
        - `suppressionType: soft_bounce_escalation` - repeated delivery failures from this company. It is company-scoped and removable.
        - `suppressionType: complaint` - the recipient reported this company's email as spam. It is company-scoped and protected.

        The platform-wide list is never exposed: a global row is returned only when the address is already associated with the authenticated company.
      operationId: listRecipientSuppressions
      tags:
        - Suppressions
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
            maxLength: 255
          description: Case-insensitive substring filter on the recipient email address.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: 1-based page number.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 25
            maximum: 100
          description: Entries per page (max 100).
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum: [suppressedAt, email, status]
            default: suppressedAt
          description: |
            Field to order by. `status` lists removable workspace escalations before protected
            suppressions. An unrecognized value falls back to `suppressedAt` rather than failing the
            request - read `sortBy` in the response to confirm what was applied.
        - name: order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
          description: |
            Sort direction. Defaults to `desc` for `suppressedAt` and `status`, `asc` for `email`.
      responses:
        "200":
          description: Suppressed recipients
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  suppressions:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                          format: email
                        suppressionType:
                          type: string
                          enum:
                            [
                              invalid_recipient,
                              unknown_hard_bounce,
                              soft_bounce_escalation,
                              complaint,
                            ]
                          description: Stable product-level classification independent of provider bounce sub-types.
                        reason:
                          type: string
                          enum: [bounced, complaint]
                        scope:
                          type: string
                          enum: [global, company]
                          description: global blocks every workspace; company blocks only this one.
                        source:
                          type: string
                          enum: [bounced_email, email_send_complaint]
                        bounceType:
                          type: string
                          nullable: true
                        bounceSubType:
                          type: string
                          nullable: true
                        bounceCount:
                          type: integer
                          nullable: true
                        suppressedAt:
                          type: string
                          format: date-time
                        delistable:
                          type: boolean
                          description: True only for company-scoped soft-bounce escalations.
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
                  hasMore:
                    type: boolean
                  sortBy:
                    type: string
                    enum: [suppressedAt, email, status]
                    description: The sort field actually applied.
                  sortOrder:
                    type: string
                    enum: [asc, desc]
                    description: The sort direction actually applied.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /suppressions/{email}:
    get:
      summary: Get recipient suppression
      description: Checks one exact recipient against Sequenzy's local bounce and complaint safeguards and the regional Amazon SES account-level suppression list. The lookup does not expose unrelated recipients from the shared SES account.
      operationId: getRecipientSuppression
      tags:
        - Suppressions
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
            format: email
          description: Exact recipient email address
        - name: region
          in: query
          required: false
          schema:
            type: string
            example: us-east-1
          description: Optional AWS SES region. Omit to check the default region and regions used by the company's sending domains.
      responses:
        "200":
          description: Recipient suppression status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  suppression:
                    type: object
                    properties:
                      email:
                        type: string
                        format: email
                      knownRecipient:
                        type: boolean
                      suppressed:
                        type: boolean
                      local:
                        type: object
                        properties:
                          suppressed:
                            type: boolean
                          suppressionType:
                            type: string
                            enum:
                              [
                                invalid_recipient,
                                unknown_hard_bounce,
                                soft_bounce_escalation,
                                complaint,
                              ]
                            nullable: true
                            description: Stable product-level classification. Null when no typed local suppression row exists.
                          reason:
                            type: string
                            nullable: true
                          source:
                            type: string
                            nullable: true
                          suppressionId:
                            type: string
                            nullable: true
                          scope:
                            type: string
                            enum: [global, company]
                            nullable: true
                            description: global when the block applies platform-wide, company when it is contained to this workspace. Null when the recipient is not locally suppressed.
                          delistable:
                            type: boolean
                            description: True only for a company-scoped soft-bounce escalation.
                      ses:
                        type: object
                        properties:
                          regionsChecked:
                            type: array
                            items:
                              type: string
                          entries:
                            type: array
                            items:
                              type: object
                              properties:
                                email:
                                  type: string
                                  format: email
                                region:
                                  type: string
                                reason:
                                  type: string
                                  enum: [BOUNCE, COMPLAINT]
                                lastUpdateTime:
                                  type: string
                                  format: date-time
                                  nullable: true
                                messageId:
                                  type: string
                                  nullable: true
                                feedbackId:
                                  type: string
                                  nullable: true
        "400":
          description: Invalid email address or AWS region
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      summary: Remove a workspace soft-bounce escalation
      description: Removes one company-associated recipient's workspace-scoped soft-bounce escalation and reactivates a bounced company subscriber. Global invalid-recipient and Amazon SES account-level suppressions, other companies' scoped rows, complaints, and unsubscribes are protected.
      operationId: removeRecipientSuppression
      tags:
        - Suppressions
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
            format: email
          description: Exact company-associated recipient email address
        - name: region
          in: query
          required: false
          schema:
            type: string
            example: us-east-1
          description: Optional AWS SES region used to limit the remaining-suppression inspection. It never authorizes removal of an SES account-level entry.
      responses:
        "200":
          description: Suppression cleanup result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  email:
                    type: string
                    format: email
                  removed:
                    type: boolean
                  removedLocalBounce:
                    type: boolean
                  removedSesRegions:
                    type: array
                    description: Always empty for company-authenticated removal; SES account-level suppressions are protected.
                    items:
                      type: string
                  reactivatedSubscriberIds:
                    type: array
                    items:
                      type: string
                  remainingSuppression:
                    type: object
                    description: Recipient suppression status after cleanup
        "400":
          description: Invalid email address or AWS region
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Recipient is not associated with the selected company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: The matching suppression is global, provider-level, complaint-based, or otherwise protected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Subscriber CRUD Operations
  # ============================================================================
  /subscribers:
    get:
      summary: List subscribers
      description: |
        Lists subscribers with stable pagination and optional filtering by status, free-text query, tags, list, segment, attribute, or email. Non-attribute results are ordered by createdAt descending with subscriber ID as a deterministic tie-breaker. Attribute-filtered results use ClickHouse-first cursor pagination ordered by subscriber ID ascending and do not include a total count.

        **Pulling a full audience:** every response includes `pagination.nextCursor` and `pagination.hasMore`. Follow `nextCursor` rather than incrementing `page`. Cursor pagination keeps results stable while subscribers are being created or deleted mid-pull (page numbers can skip or repeat rows as the underlying set shifts) and skips the total-count query, so `pagination.total` and `pagination.totalPages` are `null` on cursor requests. Combined with `limit=1000`, a 10,000-subscriber export takes ten requests instead of a hundred.
      operationId: listSubscribers
      tags:
        - Subscribers
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
          description: Page number. Cannot be combined with `cursor`.
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 1000
          description: Number of items per page (max 1000)
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor returned as pagination.nextCursor. Cannot be combined with `page`. Attribute-filtered requests return their own cursor, which is not interchangeable with the default-ordering cursor.
        - name: includeTotal
          in: query
          schema:
            type: string
            enum: ["false"]
          description: Pass `false` to skip the total-count query on page-numbered requests. Cursor requests always skip it.
        - name: status
          in: query
          schema:
            type: string
            enum: [active, unsubscribed, bounced, all]
          description: Filter by subscriber status. Use all to disable status filtering.
        - name: query
          in: query
          schema:
            type: string
          description: Free-text search across email, first name, last name, and tags
        - name: email
          in: query
          schema:
            type: string
          description: Legacy alias for a partial email search
        - name: tags
          in: query
          schema:
            type: string
          description: Comma-separated tag names. Subscribers must have all provided tags.
        - name: attribute
          in: query
          schema:
            type: string
          description: Custom attribute filter using attributeName:value syntax, such as plan:pro or mrr:50.
        - name: attributeOperator
          in: query
          schema:
            type: string
            enum: [is, contains, gt, gte, lt, lte, is_not_empty]
            default: is
          description: Attribute filter operator for direct cursor pagination. Use saved segments for exclusion operators such as is_not, not_contains, or is_empty.
        - name: list
          in: query
          schema:
            type: string
          description: Subscriber list ID or exact list name. The API tries ID first, then exact name.
        - name: listId
          in: query
          schema:
            type: string
          description: Filter by subscriber list ID.
        - name: listName
          in: query
          schema:
            type: string
          description: Filter by exact subscriber list name when the list ID is not known.
        - name: segmentId
          in: query
          schema:
            type: string
          description: Filter by an existing segment ID
        - name: unsubscribedAfter
          in: query
          schema:
            type: string
          description: Only return contacts whose `unsubscribedAt` is on or after this ISO 8601 date or datetime. Bare dates use UTC midnight; datetimes must include `Z` or an explicit offset. Contacts with no recorded opt-out date are excluded.
          example: "2026-07-01"
        - name: unsubscribedBefore
          in: query
          schema:
            type: string
          description: Only return contacts whose `unsubscribedAt` is on or before this ISO 8601 date or datetime. Bare dates use UTC midnight; datetimes must include `Z` or an explicit offset. Combine with `unsubscribedAfter` to audit a window of opt-outs.
          example: "2026-08-01"
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscribers:
                    type: array
                    items:
                      $ref: "#/components/schemas/Subscriber"
                  pagination:
                    $ref: "#/components/schemas/SubscriberListPagination"
        "400":
          description: Invalid status filter, malformed cursor, or cursor combined with page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Segment or list not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create or update subscriber
      description: |
        Creates a new subscriber or handles existing ones based on the `duplicateStrategy` parameter.

        **Duplicate Strategies:**
        - `skip` (default): Don't update existing subscribers
        - `merge`: Only fill in missing fields, never overwrite existing values
        - `overwrite`: Replace all fields (but never reactivate unsubscribed users)
      operationId: createSubscriber
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber unless a phone is provided (which creates a phone-only SMS contact). Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID. Unique per company when provided.
                  example: "user_123"
                firstName:
                  type: string
                  example: "John"
                lastName:
                  type: string
                  example: "Doe"
                timezone:
                  type: string
                  nullable: true
                  maxLength: 64
                  description: IANA timezone identifier (e.g. America/New_York) used for recipient-local campaign delivery. Invalid values fail with a 400 validation error; null clears the stored value.
                  example: "America/New_York"
                phone:
                  type: string
                  nullable: true
                  description: Phone number in E.164 format or national format. Stored normalized to E.164. Invalid values fail with a 400 validation error. Does not affect SMS consent. With no email or externalId, creates or matches a phone-only (SMS) contact.
                  example: "+15551234567"
                phoneCountry:
                  type: string
                  nullable: true
                  description: ISO 3166-1 alpha-2 country used to read a national-format phone, defaulting to US. A parsing hint only - the stored phoneCountry always comes from the parsed number. Sending it without phone fails with a 400 validation error.
                  example: "IT"
                smsConsent:
                  type: boolean
                  description: SMS marketing consent. true sets smsStatus to subscribed with consent source api, false sets unsubscribed, omitted leaves SMS status unchanged. Never inferred from phone presence.
                status:
                  type: string
                  enum: [active, unsubscribed, bounced]
                  default: active
                  description: Initial subscriber status.
                optInMode:
                  type: string
                  enum: [default, confirmed, double_opt_in]
                  default: default
                  description: |
                    Consent handling for this request:
                    - `default`: obey the company double opt-in setting for new active subscribers; existing unsubscribed contacts are not sent confirmation email
                    - `confirmed`: create or keep active immediately when you have verified consent
                    - `double_opt_in`: send a confirmation email and keep the contact unsubscribed until they confirm
                tags:
                  type: array
                  items:
                    type: string
                  example: ["newsletter"]
                lists:
                  type: array
                  items:
                    type: string
                  description: List IDs to add subscriber to. If not provided, subscriber follows the workspace default lists setting. If empty array, subscriber is added to NO lists.
                  example: ["list-id-1", "list-id-2"]
                customAttributes:
                  type: object
                  additionalProperties: true
                  example: { "plan": "free" }
                enrollInSequences:
                  type: boolean
                  default: true
                  description: Whether to enroll the subscriber in matching sequences. Defaults to true for API calls, or to false when createdAt is supplied.
                createdAt:
                  type: string
                  format: date-time
                  description: Original signup date, for importing history from another platform. Preserves the real date so date-relative segments are correct immediately. An existing contact's date only ever moves earlier, regardless of duplicateStrategy. Supplying this defaults enrollInSequences to false, and updatedAt is never backdated.
                  example: "2019-03-14T09:30:00Z"
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                  description: |
                    How to handle existing subscribers:
                    - `skip`: Don't update existing subscribers (default)
                    - `merge`: Only fill in missing fields, never overwrite existing values
                    - `overwrite`: Replace all fields (but never reactivate unsubscribed users)
      responses:
        "200":
          description: Subscriber created or handled based on duplicate strategy
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    allOf:
                      - $ref: "#/components/schemas/Subscriber"
                      - type: object
                        properties:
                          created:
                            type: boolean
                            description: Whether the subscriber was newly created
                            example: true
                          updated:
                            type: boolean
                            description: Whether the subscriber was updated (merge/overwrite strategies)
                            example: false
                          skipped:
                            type: boolean
                            description: Whether the subscriber was skipped (skip strategy on existing)
                            example: false
                  optIn:
                    type: object
                    description: Present when the subscriber is awaiting double opt-in confirmation.
                    properties:
                      required:
                        type: boolean
                        example: true
                      emailQueued:
                        type: boolean
                        example: true
        "400":
          description: Validation error, for example an invalid phone number
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/imports:
    post:
      summary: Create subscriber import
      operationId: createSubscriberImport
      tags:
        - Subscribers
      description: Queues an asynchronous full-record subscriber import of up to 5,000 contacts.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - subscribers
              properties:
                subscribers:
                  type: array
                  minItems: 1
                  maxItems: 5000
                  items:
                    $ref: "#/components/schemas/SubscriberImportRecord"
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                fileName:
                  type: string
                  maxLength: 255
                listIds:
                  type: array
                  items:
                    type: string
                enrollInSequences:
                  type: boolean
                  default: false
                defaultPhoneCountry:
                  type: string
                  minLength: 2
                  maxLength: 2
                smsConsent:
                  type: boolean
                optInMode:
                  type: string
                  enum: [default, confirmed, double_opt_in]
                  default: default
      responses:
        "202":
          description: Import queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  import:
                    $ref: "#/components/schemas/SubscriberImport"
                  message:
                    type: string
        "400":
          description: Invalid list or request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Import could not be queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/imports/{importId}:
    get:
      summary: Get subscriber import
      operationId: getSubscriberImport
      tags:
        - Subscribers
      description: Returns progress, counts, and failure summaries by import ID or batch ID. Every excluded row is explained - skippedReasons sums to skippedCount and failedReasons sums to failedCount.
      parameters:
        - name: importId
          in: path
          required: true
          description: Import ID or batch ID returned by the create endpoint.
          schema:
            type: string
      responses:
        "200":
          description: Import status returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  import:
                    $ref: "#/components/schemas/SubscriberImport"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber import not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /subscribers/external:
    get:
      summary: Get subscriber by external ID
      description: Retrieves a subscriber by their customer-owned external ID, including notes, list memberships, sequence enrollments, email stats, and recent activity.
      operationId: getSubscriberByExternalId
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: query
          required: true
          schema:
            type: string
          description: External ID. Query form supports IDs containing slashes.
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events in subscriber email stats and recent activity.
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  subscriber:
                    $ref: "#/components/schemas/DetailedSubscriber"
        "400":
          description: Missing external ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update subscriber by external ID
      description: Updates a subscriber's email, external ID, first name, last name, status, tags, or custom attributes.
      operationId: updateSubscriberByExternalId
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: query
          required: true
          schema:
            type: string
          description: External ID. Query form supports IDs containing slashes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: New delivery email. Fails with 409 if another subscriber owns it.
                externalId:
                  type: string
                  description: New external ID. Fails with 409 if another subscriber owns it.
                firstName:
                  type: string
                lastName:
                  type: string
                timezone:
                  type: string
                  nullable: true
                  maxLength: 64
                  description: IANA timezone identifier (e.g. America/New_York) used for recipient-local campaign delivery. Invalid values fail with a 400 validation error; null clears the stored value.
                  example: "America/New_York"
                phone:
                  type: string
                  nullable: true
                  description: Phone number in E.164 format or national format. Stored normalized to E.164. Invalid values fail with a 400 validation error. Does not affect SMS consent. Changing it resets SMS consent unless smsConsent is sent in the same request. null or "" clears the phone, except on a phone-only (SMS) contact, where clearing its only identity fails with a 400 validation error.
                  example: "+15551234567"
                phoneCountry:
                  type: string
                  nullable: true
                  description: ISO 3166-1 alpha-2 country used to read a national-format phone, defaulting to US. A parsing hint only - the stored phoneCountry always comes from the parsed number. Sending it without phone fails with a 400 validation error.
                  example: "IT"
                smsConsent:
                  type: boolean
                  description: SMS marketing consent. true sets smsStatus to subscribed with consent source api, false sets unsubscribed, omitted leaves SMS status unchanged. Never inferred from phone presence.
                status:
                  type: string
                  enum: [active, unsubscribed, bounced]
                  description: Setting `unsubscribed` performs the unsubscribe workflow.
                tags:
                  type: array
                  items:
                    type: string
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Custom attributes to update. Defaults to replacing the existing public custom-attribute map.
                customAttributesStrategy:
                  type: string
                  enum: [replace, merge]
                  default: replace
                  description: How to apply customAttributes. replace replaces the existing public custom-attribute map. merge overwrites only provided keys and retains unspecified existing keys.
      responses:
        "200":
          description: Subscriber updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  subscriber:
                    $ref: "#/components/schemas/Subscriber"
        "400":
          description: Missing external ID or validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Identity conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete subscriber by external ID
      description: Deletes a subscriber by their customer-owned external ID.
      operationId: deleteSubscriberByExternalId
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: query
          required: true
          schema:
            type: string
          description: External ID. Query form supports IDs containing slashes.
      responses:
        "200":
          description: Subscriber deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted:
                    type: boolean
        "400":
          description: Missing external ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /subscribers/external/{externalId}:
    get:
      summary: Get subscriber by external ID path
      description: Compatibility route for external IDs that do not contain path separators. Use `/subscribers/external?externalId=...` for IDs containing slashes.
      operationId: getSubscriberByExternalIdPath
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded external ID without path separators
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events in subscriber email stats and recent activity.
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  subscriber:
                    $ref: "#/components/schemas/DetailedSubscriber"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update subscriber by external ID path
      description: Compatibility route for external IDs that do not contain path separators. Use `/subscribers/external?externalId=...` for IDs containing slashes.
      operationId: updateSubscriberByExternalIdPath
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded external ID without path separators
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: New delivery email. Fails with 409 if another subscriber owns it.
                externalId:
                  type: string
                  description: New external ID. Fails with 409 if another subscriber owns it.
                firstName:
                  type: string
                lastName:
                  type: string
                timezone:
                  type: string
                  nullable: true
                  maxLength: 64
                  description: IANA timezone identifier (e.g. America/New_York) used for recipient-local campaign delivery. Invalid values fail with a 400 validation error; null clears the stored value.
                  example: "America/New_York"
                phone:
                  type: string
                  nullable: true
                  description: Phone number in E.164 format or national format. Stored normalized to E.164. Invalid values fail with a 400 validation error. Does not affect SMS consent. Changing it resets SMS consent unless smsConsent is sent in the same request. null or "" clears the phone, except on a phone-only (SMS) contact, where clearing its only identity fails with a 400 validation error.
                  example: "+15551234567"
                phoneCountry:
                  type: string
                  nullable: true
                  description: ISO 3166-1 alpha-2 country used to read a national-format phone, defaulting to US. A parsing hint only - the stored phoneCountry always comes from the parsed number. Sending it without phone fails with a 400 validation error.
                  example: "IT"
                smsConsent:
                  type: boolean
                  description: SMS marketing consent. true sets smsStatus to subscribed with consent source api, false sets unsubscribed, omitted leaves SMS status unchanged. Never inferred from phone presence.
                status:
                  type: string
                  enum: [active, unsubscribed, bounced]
                  description: Setting `unsubscribed` performs the unsubscribe workflow.
                tags:
                  type: array
                  items:
                    type: string
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Custom attributes to update. Defaults to replacing the existing public custom-attribute map.
                customAttributesStrategy:
                  type: string
                  enum: [replace, merge]
                  default: replace
                  description: How to apply customAttributes. replace replaces the existing public custom-attribute map. merge overwrites only provided keys and retains unspecified existing keys.
      responses:
        "200":
          description: Subscriber updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  subscriber:
                    $ref: "#/components/schemas/Subscriber"
        "409":
          description: Identity conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete subscriber by external ID path
      description: Compatibility route for external IDs that do not contain path separators. Use `/subscribers/external?externalId=...` for IDs containing slashes.
      operationId: deleteSubscriberByExternalIdPath
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded external ID without path separators
      responses:
        "200":
          description: Subscriber deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted:
                    type: boolean
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /subscribers/external/notes:
    get:
      summary: List subscriber notes by external ID
      description: Lists internal notes for a subscriber identified by customer-owned external ID.
      operationId: listSubscriberNotesByExternalId
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: query
          required: true
          schema:
            type: string
          description: External ID. Query form supports IDs containing slashes.
      responses:
        "200":
          description: Subscriber notes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  notes:
                    type: array
                    items:
                      $ref: "#/components/schemas/SubscriberNote"
        "400":
          description: Missing external ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create subscriber note by external ID
      description: Creates an internal note for a subscriber identified by customer-owned external ID.
      operationId: createSubscriberNoteByExternalId
      tags:
        - Subscribers
      parameters:
        - name: externalId
          in: query
          required: true
          schema:
            type: string
          description: External ID. Query form supports IDs containing slashes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  maxLength: 5000
                  description: Internal note body.
      responses:
        "200":
          description: Subscriber note created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  note:
                    $ref: "#/components/schemas/SubscriberNote"
        "400":
          description: Missing external ID or invalid note body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/notes/{noteId}:
    delete:
      summary: Delete subscriber note
      description: Deletes one internal subscriber note by note ID.
      operationId: deleteSubscriberNote
      tags:
        - Subscribers
      parameters:
        - name: noteId
          in: path
          required: true
          schema:
            type: string
          description: Subscriber note ID.
      responses:
        "200":
          description: Subscriber note deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted:
                    type: boolean
                  id:
                    type: string
                  subscriberId:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Note not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /subscribers/{email}/notes:
    get:
      summary: List subscriber notes
      description: Lists internal notes for a subscriber identified by email address.
      operationId: listSubscriberNotes
      tags:
        - Subscribers
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded email address
      responses:
        "200":
          description: Subscriber notes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  notes:
                    type: array
                    items:
                      $ref: "#/components/schemas/SubscriberNote"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create subscriber note
      description: Creates an internal note for a subscriber identified by email address.
      operationId: createSubscriberNote
      tags:
        - Subscribers
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded email address
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  maxLength: 5000
                  description: Internal note body.
      responses:
        "200":
          description: Subscriber note created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  note:
                    $ref: "#/components/schemas/SubscriberNote"
        "400":
          description: Invalid note body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/{email}:
    get:
      summary: Get subscriber by email
      description: Retrieves a subscriber by their email address, including notes, list memberships, sequence enrollments, email stats, and recent activity.
      operationId: getSubscriber
      tags:
        - Subscribers
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded email address
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events in subscriber email stats and recent activity.
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    $ref: "#/components/schemas/DetailedSubscriber"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: "Subscriber not found"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update subscriber
      description: Updates a subscriber's first name, last name, status, tags, or custom attributes. Setting `status` to `unsubscribed` performs the full unsubscribe workflow, including list unsubscription and sequence cancellation.
      operationId: updateSubscriber
      tags:
        - Subscribers
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded email address
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: New delivery email. Fails with 409 if another subscriber owns it.
                externalId:
                  type: string
                  description: New customer-owned external ID. Fails with 409 if another subscriber owns it.
                firstName:
                  type: string
                  example: "Jane"
                lastName:
                  type: string
                  example: "Smith"
                phone:
                  type: string
                  nullable: true
                  description: Phone number in E.164 format or national format. Stored normalized to E.164. Invalid values fail with a 400 validation error. Does not affect SMS consent. Changing it resets SMS consent unless smsConsent is sent in the same request. null or "" clears the phone, except on a phone-only (SMS) contact, where clearing its only identity fails with a 400 validation error.
                  example: "+15551234567"
                phoneCountry:
                  type: string
                  nullable: true
                  description: ISO 3166-1 alpha-2 country used to read a national-format phone, defaulting to US. A parsing hint only - the stored phoneCountry always comes from the parsed number. Sending it without phone fails with a 400 validation error.
                  example: "IT"
                smsConsent:
                  type: boolean
                  description: SMS marketing consent. true sets smsStatus to subscribed with consent source api, false sets unsubscribed, omitted leaves SMS status unchanged. Never inferred from phone presence.
                status:
                  type: string
                  enum: [active, unsubscribed, bounced]
                  description: Setting `unsubscribed` performs a full global unsubscribe.
                tags:
                  type: array
                  items:
                    type: string
                  example: ["premium", "vip"]
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Custom attributes to update. Defaults to replacing the existing public custom-attribute map.
                customAttributesStrategy:
                  type: string
                  enum: [replace, merge]
                  default: replace
                  description: How to apply customAttributes. replace replaces the existing public custom-attribute map. merge overwrites only provided keys and retains unspecified existing keys.
      responses:
        "200":
          description: Subscriber updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    $ref: "#/components/schemas/Subscriber"
        "400":
          description: Validation error, for example an invalid phone number
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      summary: Delete subscriber
      description: Deletes a subscriber by their email address.
      operationId: deleteSubscriber
      tags:
        - Subscribers
      parameters:
        - name: email
          in: path
          required: true
          schema:
            type: string
          description: URL-encoded email address
      responses:
        "200":
          description: Subscriber deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  deleted:
                    type: boolean
                    example: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Tag Operations
  # ============================================================================
  /subscribers/tags:
    post:
      summary: Add tag to subscriber
      description: Adds a tag to a subscriber. Creates the subscriber if they don't exist. Creates the tag definition if it doesn't exist. When the workspace has double opt-in enabled, a brand-new subscriber is created pending confirmation, the confirmation email is queued, and tag automations wait at their trigger until the subscriber confirms.
      operationId: addTag
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tag
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber. Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID
                  example: "user_123"
                firstName:
                  type: string
                  description: First name to set if creating the subscriber.
                  example: "John"
                lastName:
                  type: string
                  description: Last name to set if creating the subscriber.
                  example: "Doe"
                tag:
                  type: string
                  minLength: 1
                  example: "premium"
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Optional attributes to set on the subscriber if created
      responses:
        "200":
          description: Tag added successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                        nullable: true
                        description: Null for phone-only (SMS) contacts.
                      tags:
                        type: array
                        items:
                          type: string
                      created:
                        type: boolean
                        description: Whether the subscriber was newly created
                  tag:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      created:
                        type: boolean
                        description: Whether the tag definition was newly created
                  optIn:
                    type: object
                    description: Present when this request created a brand-new subscriber while workspace double opt-in is enabled. The tag is applied, but the subscriber stays pending and tag automations wait until they confirm.
                    properties:
                      required:
                        type: boolean
                      emailQueued:
                        type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/tags/bulk:
    post:
      summary: Add multiple tags to subscriber
      description: Adds multiple tags to a subscriber. Creates the subscriber if they don't exist. Creates tag definitions if they don't exist. When the workspace has double opt-in enabled, a brand-new subscriber is created pending confirmation, the confirmation email is queued, and tag automations wait at their trigger until the subscriber confirms.
      operationId: addTagsBulk
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tags
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber. Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID
                  example: "user_123"
                firstName:
                  type: string
                  description: First name to set if creating the subscriber.
                  example: "John"
                lastName:
                  type: string
                  description: Last name to set if creating the subscriber.
                  example: "Doe"
                tags:
                  type: array
                  items:
                    type: string
                    minLength: 1
                  minItems: 1
                  example: ["premium", "newsletter", "vip"]
                customAttributes:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Tags added successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                        nullable: true
                        description: Null for phone-only (SMS) contacts.
                      tags:
                        type: array
                        items:
                          type: string
                      created:
                        type: boolean
                  tags:
                    type: object
                    properties:
                      added:
                        type: array
                        items:
                          type: string
                        description: Tag names that were added
                      created:
                        type: array
                        items:
                          type: string
                        description: Tag definitions that were newly created
                  optIn:
                    type: object
                    description: Present when this request created a brand-new subscriber while workspace double opt-in is enabled. The tags are applied, but the subscriber stays pending and tag automations wait until they confirm.
                    properties:
                      required:
                        type: boolean
                      emailQueued:
                        type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/tags/remove:
    post:
      summary: Remove tag from subscriber
      description: Removes a tag from a subscriber. Creates the subscriber if they don't exist (without the tag).
      operationId: removeTag
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tag
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber. Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID
                  example: "user_123"
                tag:
                  type: string
                  minLength: 1
                  example: "premium"
                firstName:
                  type: string
                  description: First name (used if creating new subscriber)
                lastName:
                  type: string
                  description: Last name (used if creating new subscriber)
      responses:
        "200":
          description: Tag removed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                        nullable: true
                        description: Null for phone-only (SMS) contacts.
                      tags:
                        type: array
                        items:
                          type: string
                      created:
                        type: boolean
                        description: Whether the subscriber was newly created
                  tag:
                    type: object
                    properties:
                      name:
                        type: string
                      removed:
                        type: boolean
                        description: Whether the tag was removed from the subscriber
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/bulk/tags/add:
    post:
      summary: Add tags to many subscribers
      description: Adds one or more tags to up to 500 existing subscribers identified by email, externalId, or subscriberId. Built for reconciling historical or derived tags, so identifiers that do not match an existing subscriber are returned in notFound rather than creating contacts. Tag automations are skipped unless triggerAutomations is true, which requires the automations:trigger scope.
      operationId: bulkAddSubscriberTags
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkSubscriberTagRequest"
            examples:
              backfill:
                summary: Backfill a derived tag without enrolling anyone
                value:
                  tags: ["derived-churn-risk"]
                  emails: ["one@example.com", "two@example.com"]
      responses:
        "200":
          description: Bulk tag update completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkSubscriberTagResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: triggerAutomations requested without the automations:trigger scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/bulk/tags/remove:
    post:
      summary: Remove tags from many subscribers
      description: Removes one or more tags from up to 500 existing subscribers identified by email, externalId, or subscriberId. Identifiers that do not match an existing subscriber are returned in notFound.
      operationId: bulkRemoveSubscriberTags
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkSubscriberTagRequest"
            examples:
              rollback:
                summary: Roll back a derived tag backfill
                value:
                  tags: ["derived-churn-risk"]
                  subscriberIds: ["sub_abc123", "sub_def456"]
      responses:
        "200":
          description: Bulk tag update completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkSubscriberTagResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Event Operations
  # ============================================================================
  /subscribers/events:
    post:
      summary: Trigger event for subscriber
      description: Triggers an event for a subscriber. Creates the subscriber if they don't exist and applies the workspace default lists setting. Creates the event definition if it doesn't exist. When the workspace has double opt-in enabled, a brand-new subscriber is created pending confirmation, the confirmation email is queued, and matching sequences wait at their trigger until the subscriber confirms.
      operationId: triggerEvent
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - event
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber. Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID
                  example: "user_123"
                firstName:
                  type: string
                  description: First name to set if creating the subscriber.
                  example: "John"
                lastName:
                  type: string
                  description: Last name to set if creating the subscriber.
                  example: "Doe"
                event:
                  type: string
                  minLength: 1
                  example: "purchase.completed"
                properties:
                  type: object
                  additionalProperties: true
                  description: Event properties/metadata
                  example:
                    {
                      "amount": 9900,
                      "currency": "USD",
                      "productId": "prod_123",
                    }
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Optional attributes to set on the subscriber if created
                occurredAt:
                  type: string
                  format: date-time
                  description: When the event actually happened. Defaults to now. More than an hour in the past records it as history - stored with the real timestamp and counted by segments, but running no sequences, sync rules, waiting steps, goal conversions or webhooks, and the response carries historical=true. Older than the 5-year event retention window is rejected with 400.
                  example: "2024-11-02T10:00:00Z"
                eventId:
                  type: string
                  description: Caller-owned event ID used as an idempotency key on both paths. A repeated live event returns the existing event with duplicate=true. A repeated historical event remains a historical response and increments duplicates instead. Best-effort for live events sent within about a second of each other, so a producer needing a strict guarantee should keep its own ledger.
                  example: "inv_9182"
      responses:
        "200":
          description: Event triggered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                        nullable: true
                        description: Null for phone-only (SMS) contacts.
                      created:
                        type: boolean
                  event:
                    type: object
                    properties:
                      id:
                        type: string
                        description: The created event record ID
                      name:
                        type: string
                      definitionCreated:
                        type: boolean
                        description: Whether the event definition was newly created
                  duplicate:
                    type: boolean
                    description: Present and true when a live event's supplied eventId was already recorded for this contact and event name. Nothing was written and no side effects ran; event holds the existing event. Historical responses use duplicates instead.
                    example: true
                  historical:
                    type: boolean
                    description: Present and true when occurredAt selected the historical import path.
                    example: true
                  events:
                    type: array
                    description: Historical event results. Present instead of event on the historical path.
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        occurredAt:
                          type: string
                          format: date-time
                  inserted:
                    type: integer
                    description: Historical event rows inserted by this request.
                    example: 1
                  duplicates:
                    type: integer
                    description: Historical event rows skipped because their idempotency receipt already existed.
                    example: 0
                  sideEffectFailures:
                    type: array
                    items:
                      type: string
                    description: Present when the event was recorded but one or more side-effect stages (e.g. apply-sync-rules, trigger-event-automations) failed. Retry-sensitive callers should treat these as partial failures.
                    example: ["apply-sync-rules"]
                  optIn:
                    type: object
                    description: Present when this event created a brand-new subscriber while workspace double opt-in is enabled. The subscriber stays pending and matching sequences wait until they confirm.
                    properties:
                      required:
                        type: boolean
                      emailQueued:
                        type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /subscribers/events/bulk:
    post:
      summary: Trigger multiple events for subscriber
      description: Triggers multiple events for a subscriber. Creates the subscriber if they don't exist and applies the workspace default lists setting. Creates event definitions if they don't exist. Events are processed independently, so an error response may still include events that were already triggered. When the workspace has double opt-in enabled, a brand-new subscriber is created pending confirmation, a single confirmation email is queued for the request, and matching sequences wait at their trigger until the subscriber confirms.
      operationId: triggerEventsBulk
      tags:
        - Subscribers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - events
              properties:
                email:
                  type: string
                  format: email
                  description: Required when creating a new subscriber. Optional when externalId identifies an existing subscriber.
                  example: "user@example.com"
                externalId:
                  type: string
                  description: Customer-owned app/customer/user ID
                  example: "user_123"
                firstName:
                  type: string
                  description: First name to set if creating the subscriber.
                  example: "John"
                lastName:
                  type: string
                  description: Last name to set if creating the subscriber.
                  example: "Doe"
                events:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    type: object
                    required:
                      - name
                    properties:
                      name:
                        type: string
                        minLength: 1
                        example: "page.viewed"
                      properties:
                        type: object
                        additionalProperties: true
                        example: { "page": "/pricing" }
                      occurredAt:
                        type: string
                        format: date-time
                        description: When this event actually happened. Defaults to now. When every event in the batch is more than an hour old the batch is imported as history in one idempotent write, running no sequences, sync rules, waiting steps, goal conversions or webhooks.
                        example: "2024-11-02T10:00:00Z"
                      eventId:
                        type: string
                        description: Caller-owned event ID that makes a re-run idempotent on both the live and historical paths. On the live path a repeated ID is skipped and its response entry carries duplicate=true.
                        example: "inv_9182"
                customAttributes:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Events triggered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriber:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                        nullable: true
                        description: Null for phone-only (SMS) contacts.
                      created:
                        type: boolean
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        definitionCreated:
                          type: boolean
                        duplicate:
                          type: boolean
                          description: Present and true when this event's eventId was already recorded for the contact and event name, so nothing was written and no side effects ran.
                          example: true
                        sideEffectFailures:
                          type: array
                          items:
                            type: string
                          description: Present when this event was recorded but one or more side-effect stages (e.g. apply-sync-rules, trigger-event-automations) failed.
                  optIn:
                    type: object
                    description: Present when this request created a brand-new subscriber while workspace double opt-in is enabled. The subscriber stays pending and matching sequences wait until they confirm.
                    properties:
                      required:
                        type: boolean
                      emailQueued:
                        type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Segment Operations
  # ============================================================================
  /segments:
    get:
      summary: List segments
      description: Lists saved segments and subscriber counts for the authenticated company.
      operationId: listSegments
      tags:
        - Segments
      responses:
        "200":
          description: Segments returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  segments:
                    type: array
                    items:
                      $ref: "#/components/schemas/Segment"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create segment
      description: Creates a saved segment from either flat filters or a nested filter root.
      operationId: createSegment
      tags:
        - Segments
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                filters:
                  type: array
                  description: Legacy v1 flat filter shape. Provide either filters or root.
                  items:
                    $ref: "#/components/schemas/FilterLeaf"
                filterJoinOperator:
                  type: string
                  enum: [and, or]
                root:
                  description: Nested v2 filter tree. Provide either root or filters.
                  $ref: "#/components/schemas/FilterGroup"
              required:
                - name
              anyOf:
                - required:
                    - filters
                - required:
                    - root
      responses:
        "200":
          description: Segment created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  segment:
                    $ref: "#/components/schemas/Segment"
                  warnings:
                    type: array
                    description: Non-fatal cautions, such as an attribute filter referencing a custom attribute no subscriber has a synced value for (which would match no subscribers, or every subscriber for exclusion operators like is_empty). Absent when there is nothing to warn about.
                    items:
                      type: string
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidTagOperator:
                  value:
                    success: false
                    error: 'Operator "is not" is not supported for Tag filters. Use one of: contains, does not contain, is empty, is not empty.'
        "409":
          description: Segment name conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentFriendlyError"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /segments/{segmentId}:
    patch:
      summary: Update segment
      description: Updates a saved segment's name or filter definition.
      operationId: updateSegment
      tags:
        - Segments
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                filters:
                  type: array
                  items:
                    $ref: "#/components/schemas/FilterLeaf"
                filterJoinOperator:
                  type: string
                  enum: [and, or]
                root:
                  $ref: "#/components/schemas/FilterGroup"
      responses:
        "200":
          description: Segment updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  segment:
                    $ref: "#/components/schemas/Segment"
                  warnings:
                    type: array
                    description: Non-fatal cautions, such as an attribute filter referencing a custom attribute no subscriber has a synced value for (which would match no subscribers, or every subscriber for exclusion operators like is_empty). Absent when there is nothing to warn about.
                    items:
                      type: string
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                invalidTagOperator:
                  value:
                    success: false
                    error: 'Operator "is not" is not supported for Tag filters. Use one of: contains, does not contain, is empty, is not empty.'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteSegment
      summary: Delete segment
      description: Deletes a saved segment. Subscribers matched by the segment are not affected.
      tags:
        - Segments
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
          description: Segment ID.
      responses:
        "200":
          description: Segment deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /segments/{segmentId}/count:
    get:
      summary: Get segment count
      description: Returns the current subscriber count for a saved segment.
      operationId: getSegmentCount
      tags:
        - Segments
      parameters:
        - name: segmentId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Segment count returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  segmentId:
                    type: string
                  count:
                    type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Migration Gateway
  # ============================================================================
  /migrations/{runId}:
    get:
      summary: Get migration run
      description: Returns provider-neutral migration status, discovery, plan, progress, and report.
      operationId: getMigrationRun
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Migration run returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
              example:
                success: true
                data:
                  id: run_123
                  status: awaiting_approval
                  discovery:
                    provider: kit
                    resources:
                      - id: subscribers
                        type: subscribers
                        count: 3470
                      - id: campaigns
                        type: campaigns
                        count: 108
                      - id: automations
                        type: automations
                        count: 42
                  approvedPlan: {}
                  progress: {}
                  report:
                    subscribersQueued: 0
                    draftCampaignsCreated: 0
                    draftSequencesCreated: 0
                    draftTransactionalEmailsCreated: 0
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /migrations/{runId}/agent-package:
    get:
      summary: Get migration agent package
      description: Returns endpoint URLs and exact call sequence for an agent-assisted migration.
      operationId: getMigrationAgentPackage
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Agent package returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
              example:
                success: true
                data:
                  runId: run_123
                  provider: kit
                  providerLabel: Kit
                  systemPrompt: "Preserve provider email HTML exactly..."
                  instructions:
                    - Do not convert migrated HTML into Sequenzy blocks or recreate the email from its visible text.
                  endpoints:
                    connectSource: https://api.sequenzy.com/api/v1/migrations/run_123/connect-source
                  sequence:
                    - POST connect-source with the provider credential.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /migrations/{runId}/connect-source:
    post:
      summary: Connect migration source
      description: Stores a provider credential on an existing migration run connection.
      operationId: connectMigrationSource
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - provider
                - credential
              properties:
                provider:
                  type: string
                  description: Provider adapter ID. Supported values include `active-campaign`, `brevo`, `constant-contact`, `customer-io`, `drip`, `hubspot`, `kit`, `klaviyo`, `loops`, `mailchimp`, `mailerlite`, `mailjet`, `omnisend`, `resend`, and `sendgrid`.
                credential:
                  type: string
                  description: Provider API credential.
                providerLabel:
                  type: string
                  description: Optional display label for manual providers.
      responses:
        "200":
          description: Source connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        "400":
          description: Provider unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /migrations/{runId}/discover:
    post:
      summary: Discover migration source
      description: Queues provider discovery for a migration run.
      operationId: discoverMigrationSource
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Discovery queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /migrations/{runId}/approve-plan:
    post:
      summary: Approve migration plan
      description: Approves provider-neutral resources and freezes the execution plan for a migration run.
      operationId: approveMigrationPlan
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - resourceIds
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                resourceOptions:
                  type: object
                  additionalProperties:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: Plan approved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        "400":
          description: Invalid approval
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Approval failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /migrations/{runId}/start:
    post:
      summary: Start migration
      description: Queues execution for an approved migration run.
      operationId: startMigration
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                resourceIds:
                  type: array
                  items:
                    type: string
                resourceOptions:
                  type: object
                  additionalProperties:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: Migration queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /migrations/{runId}/cancel:
    post:
      summary: Cancel migration
      description: Cancels queued/pre-execution runs immediately. Running imports move to cancel_requested while workers stop linked subscriber import chunks, then finish as canceled.
      operationId: cancelMigration
      tags:
        - Migrations
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Migration canceled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Migration run not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Audience Sync Operations (segment -> Meta custom audience)
  # ============================================================================
  /audience-syncs:
    get:
      summary: List audience syncs
      description: Lists segment-to-Meta-audience syncs with schedule and last sync status.
      operationId: listAudienceSyncs
      tags:
        - Audience Syncs
      responses:
        "200":
          description: Audience syncs returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  audienceSyncs:
                    type: array
                    items:
                      $ref: "#/components/schemas/AudienceSync"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create audience sync
      description: >-
        Pushes a segment to a Meta custom audience and keeps it synced on a
        schedule. Provide segmentId for an existing segment or
        predefinedSegmentId for a ready-made template (for example zero-ltv,
        no-purchase-1y, recent-buyers); template segments are created
        automatically on first use. The first upload runs immediately.
        Audiences are add-only - subscribers who later leave the segment stay
        in the Meta audience. Requires the Meta Ads integration to be
        connected in the dashboard.
      operationId: createAudienceSync
      tags:
        - Audience Syncs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                segmentId:
                  type: string
                  description: Existing segment ID. Provide either segmentId or predefinedSegmentId, not both.
                predefinedSegmentId:
                  type: string
                  description: >-
                    Ready-made segment template ID (for example recent-buyers).
                    The template segment is created on first use and reused
                    afterwards; an unrelated same-named segment with different
                    filters causes a 409.
                adAccountId:
                  type: string
                  description: Meta ad account ID (act_...). See GET /ad-accounts.
                audienceName:
                  type: string
                  description: Name for the custom audience in Meta Ads Manager.
                frequency:
                  type: string
                  enum: [hourly, daily, weekly]
                  default: daily
              required:
                - adAccountId
                - audienceName
              anyOf:
                - required:
                    - segmentId
                - required:
                    - predefinedSegmentId
      responses:
        "200":
          description: Audience sync created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  audienceSync:
                    $ref: "#/components/schemas/AudienceSync"
        "400":
          description: >-
            Validation error (for example both segmentId and
            predefinedSegmentId provided) or Meta Ads not connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: >-
            Segment already synced to that ad account, or the template's name
            is taken by an unrelated segment with different filters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /audience-syncs/{syncId}:
    patch:
      summary: Update audience sync
      description: Changes an audience sync's frequency or pauses/resumes it.
      operationId: updateAudienceSync
      tags:
        - Audience Syncs
      parameters:
        - name: syncId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                frequency:
                  type: string
                  enum: [hourly, daily, weekly]
                isActive:
                  type: boolean
                  description: false pauses the sync, true resumes it.
      responses:
        "200":
          description: Audience sync updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  audienceSync:
                    $ref: "#/components/schemas/AudienceSync"
        "404":
          description: Audience sync not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete audience sync
      description: >-
        Removes the sync mapping. The Meta audience itself is kept so running
        ads are not disrupted - only future syncs stop.
      operationId: deleteAudienceSync
      tags:
        - Audience Syncs
      parameters:
        - name: syncId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Audience sync deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "404":
          description: Audience sync not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /audience-syncs/{syncId}/sync:
    post:
      summary: Run audience sync now
      description: Triggers an immediate upload outside the regular schedule. The sync must be active.
      operationId: runAudienceSyncNow
      tags:
        - Audience Syncs
      parameters:
        - name: syncId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sync queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  queued:
                    type: boolean
                    example: true
        "400":
          description: Sync is paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Audience sync not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /ad-accounts:
    get:
      summary: List ad accounts
      description: Lists the Meta ad accounts reachable through the connected Meta Ads integration.
      operationId: listAdAccounts
      tags:
        - Audience Syncs
      responses:
        "200":
          description: Ad accounts returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  adAccounts:
                    type: array
                    items:
                      $ref: "#/components/schemas/AdAccount"
        "400":
          description: Meta Ads not connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Transactional Email Operations
  # ============================================================================
  /transactional:
    get:
      summary: List transactional emails
      description: Lists transactional email templates with their linked subjects and all-time delivery metrics. Search name, slug, or subject; filter active state; and sort by engagement. Human engagement is used by default.
      operationId: listTransactionalEmails
      tags:
        - Transactional
      parameters:
        - name: search
          in: query
          schema:
            type: string
          description: Case-insensitive search across template name, API slug, and linked email subject/title.
        - name: status
          in: query
          schema:
            type: string
            enum: [all, active, disabled]
            default: all
          description: Filter by template active state.
        - name: sort
          in: query
          schema:
            type: string
            enum: [date, sends, opens, open-rate, clicks, ctr]
            default: date
          description: Sort by creation date or all-time engagement metrics.
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort direction.
        - name: includeMachineEngagement
          in: query
          schema:
            type: boolean
            default: false
          description: Include detected bot, scanner, preview, and privacy-proxy engagement in open and click metrics.
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  transactional:
                    type: array
                    items:
                      $ref: "#/components/schemas/TransactionalEmailListItem"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create transactional email
      description: Creates a saved transactional email template from exactly one of prompt, HTML, or Sequenzy blocks. Prompt-created templates default to disabled.
      operationId: createTransactionalEmail
      tags:
        - Transactional
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  required:
                    - name
                  properties:
                    name:
                      type: string
                      example: "Password Reset"
                    slug:
                      type: string
                      description: Optional API slug used when sending by slug. If omitted, one is generated from the name.
                      example: "password-reset"
                    subject:
                      type: string
                      description: Required with HTML or blocks; optional with prompt, where it overrides the generated subject.
                      example: "Reset your password, {{NAME}}"
                    previewText:
                      type: string
                      nullable: true
                      example: "Use this link to reset your password."
                    prompt:
                      type: string
                      minLength: 1
                      description: Natural-language request for branded transactional blocks.
                    style:
                      type: string
                      description: Generation style; valid only with prompt.
                    tone:
                      type: string
                      description: Generation tone; valid only with prompt.
                    enabled:
                      type: boolean
                      description: Defaults to false with prompt and true with explicit HTML or blocks.
                      example: true
      responses:
        "201":
          description: Transactional email created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  transactional:
                    $ref: "#/components/schemas/TransactionalEmailDetails"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing-content:
                  summary: Missing content source
                  value:
                    success: false
                    error: "Provide exactly one of prompt, html, or blocks."
                mixed-content:
                  summary: Content sources cannot be mixed
                  value:
                    success: false
                    error: "Provide exactly one of prompt, html, or blocks."
                missing-sender:
                  summary: Sender profile is required
                  value:
                    success: false
                    error: "No sender profile configured. Please create a sender profile first."
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Duplicate slug
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: 'A transactional email with slug "password-reset" already exists'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /transactional/{idOrSlug}:
    get:
      summary: Get transactional email
      description: Gets details of a transactional email template by ID or slug, including linked body content and available template variables.
      operationId: getTransactionalEmail
      tags:
        - Transactional
      parameters:
        - name: idOrSlug
          in: path
          required: true
          schema:
            type: string
          description: Transactional email ID or slug
          example: "welcome-email"
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  transactional:
                    $ref: "#/components/schemas/TransactionalEmailDetails"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Transactional email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: 'Transactional email "welcome-email" not found'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update transactional email
      description: Updates transactional email metadata or replaces the linked email body using raw HTML or Sequenzy blocks.
      operationId: updateTransactionalEmail
      tags:
        - Transactional
      parameters:
        - name: idOrSlug
          in: path
          required: true
          schema:
            type: string
          description: Transactional email ID or slug
          example: "welcome-email"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  properties:
                    name:
                      type: string
                      example: "Welcome Email"
                    enabled:
                      type: boolean
                      example: true
                    subject:
                      type: string
                      example: "Welcome, {{NAME}}"
                    previewText:
                      type: string
                      nullable: true
                      example: "Your account is ready."
      responses:
        "200":
          description: Transactional email updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  transactional:
                    $ref: "#/components/schemas/TransactionalEmailDetails"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Transactional email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete transactional email
      description: |
        Permanently deletes a saved transactional email template by ID or slug, so its slug stops sending and becomes free to reuse.

        Already-sent deliveries are untouched: send history, stats, and stored HTML live on the deliveries themselves.

        The email content is kept as a reusable template and returned as `deleted.emailId`; pass that to `DELETE /api/v1/templates/{templateId}` to remove the content too. To stop sends without losing the template, update it with `enabled: false` instead.

        Requires an API key with the `transactional:delete` scope.
      operationId: deleteTransactionalEmail
      tags:
        - Transactional
      parameters:
        - name: idOrSlug
          in: path
          required: true
          schema:
            type: string
          description: Transactional email ID or slug
          example: "welcome-email"
      responses:
        "200":
          description: Transactional email deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Transactional email deleted"
                  deleted:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "txn_abc123"
                      slug:
                        type: string
                        example: "welcome-email"
                      name:
                        type: string
                        example: "Welcome Email"
                      emailId:
                        type: string
                        description: The email content kept as a reusable template. Delete it separately with `DELETE /api/v1/templates/{templateId}`.
                        example: "email_abc123"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing the transactional:delete scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Transactional email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: 'Transactional email "welcome-email" not found'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /transactional/send:
    post:
      summary: Send transactional or marketing email
      description: |
        Queues an email for sending. The default `emailType` is `transactional`. Set it to `marketing` for a consented single-recipient lifecycle or promotional message. Marketing mode creates or links a minimal subscriber, honors unsubscribe suppression, adds the standard marketing footer, and emits RFC 8058 one-click-unsubscribe headers. The caller remains responsible for having consent or another lawful basis.

        For callers that may retry, send a stable `Idempotency-Key` header. The same key and request returns the original `emailSendId` for 14 days without another delivery. Reusing a key with different request content returns 409.

        You can either:
        - Provide a canonical `slug` (or compatibility alias `templateId`) to use a saved template
        - Provide `subject` and canonical `body` (or compatibility alias `html`) to send custom content directly

        If both a canonical field and its alias are provided, `slug` must match `templateId` and `body` must match `html`.

        **Recipients:**
        - `to` can be a single email or an array of up to 50 emails
        - Duplicate emails are automatically deduplicated
        - Marketing mode requires exactly one `to` recipient and does not support `cc` or `bcc`

        **Attachments:**
        - Attachments can be provided as Base64-encoded content or URLs
        - Maximum 10 attachments and 7MB total per email
        - Any file type supported (PDFs, images, documents, etc.)
        - Set `contentId` on an attachment to embed it as an inline image referenced from the HTML as `<img src="cid:VALUE">`

        A successful response means the email was accepted for background processing. Transactional emails are not blocked by subscriber unsubscribe or double opt-in status. If a recipient is suppressed because of a hard bounce or spam complaint, the worker records the send as `suppressed` instead of delivering it.

        Optionally set `from` (domain must be verified) and `replyTo` addresses. When reply tracking is enabled, Sequenzy uses a unique trackable `Reply-To` header and treats the resolved reply destination as the forwarding destination for captured replies.
        When `replyTo` is omitted, direct-content sends inherit the company's default reply profile and saved-template sends prefer the template reply profile before the company default. Both fall back to the first company reply profile. The resolved destination is retained whether or not reply tracking is enabled; it is sent as the Reply-To header only when reply tracking is disabled.
        Variables can be passed to customize the email content. Nested objects and arrays are supported for repeat blocks, such as `items`. `{{viewInBrowserUrl}}` is generated automatically for a hosted copy link. For a single recipient, Sequenzy matches an existing subscriber by `subscriberExternalId` or email and backfills stored first and last names when the corresponding request variables are omitted; explicit variables take precedence. Returns immediately with a durable `emailSendId` and the accepted `emailType`. If Sequenzy detects likely missing or unused variables before queueing, the successful response includes a non-blocking `diagnostics` warning object. Missing values do not block queueing or sending; a required variable that is not provided and has no default renders as an empty string.
      operationId: sendTransactionalEmail
      tags:
        - Transactional
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: Caller-owned key for one logical email. Reuse the same key and request on retries to receive the original send for 14 days. Reusing the key with different content returns 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
              properties:
                to:
                  oneOf:
                    - type: string
                      format: email
                    - type: array
                      items:
                        type: string
                        format: email
                      maxItems: 50
                  description: |
                    Recipient email address(es). Can be a single email string or an array of up to 50 emails.
                  example: "recipient@example.com"
                cc:
                  oneOf:
                    - type: string
                      format: email
                    - type: array
                      items:
                        type: string
                        format: email
                      maxItems: 50
                  description: Visible carbon-copy recipient email address(es). Duplicates already present in `to` are removed.
                  example: "manager@example.com"
                bcc:
                  oneOf:
                    - type: string
                      format: email
                    - type: array
                      items:
                        type: string
                        format: email
                      maxItems: 50
                  description: Blind-carbon-copy recipient email address(es). Duplicates already present in `to` or `cc` are removed.
                  example: "archive@example.com"
                slug:
                  type: string
                  description: Canonical slug of the transactional email template to use (mutually exclusive with direct content).
                  example: "welcome-email"
                templateId:
                  type: string
                  description: Compatibility alias for `slug`. Despite the field name, pass the saved transactional email API slug, not its database ID. Must match `slug` when both are provided.
                  example: "welcome-email"
                subscriberExternalId:
                  type: string
                  maxLength: 255
                  description: Customer-owned subscriber ID for single-recipient sends. If it matches an existing subscriber, analytics and localization use that subscriber; the value is also stored on the send and emitted as external_id in outbound email webhooks even when no subscriber exists. Maximum length is 255 characters.
                  example: "user_123"
                emailType:
                  type: string
                  enum: [transactional, marketing]
                  default: transactional
                  description: Delivery policy. Marketing mode requires one recipient, creates or links a minimal subscriber, honors unsubscribe suppression, adds the standard footer, and emits RFC 8058 List-Unsubscribe and List-Unsubscribe-Post headers.
                  example: marketing
                subject:
                  type: string
                  description: Email subject (required if not using slug)
                  example: "Welcome to our platform!"
                body:
                  type: string
                  description: Canonical email body HTML content (required if not using a template slug).
                  example: "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
                html:
                  type: string
                  description: Compatibility alias for `body`. Accepted with `subject` for direct sends and must match `body` when both are provided.
                  example: "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
                preview:
                  type: string
                  description: Preview text for the email (only used with direct content)
                  example: "Welcome to our platform"
                variables:
                  type: object
                  additionalProperties: true
                  description: Variables for template replacement (works with both modes). Values can be scalars, nested objects, or arrays used by repeat blocks. For a single recipient, stored subscriber first and last names fill missing name variables; explicit request variables take precedence. Raw HTML templates can use simple subscriber/custom-attribute conditionals such as `{{#if subscriber.plan}}...{{else}}...{{/if}}` and `{{#unless subscriber.plan}}...{{/unless}}`. Variables are always HTML-escaped; a template can prefix a tag with `html.` (`{{html.prerenderedHtml}}`) to insert a trusted HTML value unescaped. Injected HTML is sanitized (scripts, event handlers, and dangerous URLs are stripped), only applies in HTML text position, and must not contain end-user input. Likely variable issues are returned as non-blocking diagnostics when possible; missing required variables without defaults render as empty strings and do not block sending.
                  example:
                    NAME: "John"
                    event:
                      items:
                        - title: "Pro plan"
                          description: "Monthly subscription"
                from:
                  type: string
                  description: |
                    Custom from address. Format: "Name <email>" or just "email".
                    The domain must be verified for your account. If not verified, this field is silently ignored.
                  example: "Notifications <notifications@example.com>"
                replyTo:
                  type: string
                  description: |
                    Reply-to address. Format: "Name <email>" or just "email".
                    Can be any valid email address. When reply tracking is disabled, this value is sent as the email's `Reply-To` header. When reply tracking is enabled, Sequenzy sends a unique trackable `Reply-To` header and stores this value as the forwarding destination for replies.
                    When omitted, direct-content sends inherit the company default and saved-template sends prefer the template reply profile before the company default. Both fall back to the first company reply profile. The resolved destination is retained whether or not reply tracking is enabled; it is sent directly only when reply tracking is disabled.
                  example: "Support <support@example.com>"
                attachments:
                  type: array
                  maxItems: 10
                  items:
                    $ref: "#/components/schemas/Attachment"
                  description: |
                    File attachments for the email. Each attachment must have a filename and either:
                    - `content`: Base64-encoded file content
                    - `path`: URL to fetch the file from

                    Set `contentId` to embed the file as an inline image the HTML references with `<img src="cid:VALUE">` instead of attaching it.

                    Maximum 10 attachments and 7MB total per email.
                  example:
                    - filename: "invoice.pdf"
                      path: "https://example.com/invoices/123.pdf"
                trackingSettings:
                  type: object
                  description: Per-send tracking opt-outs. Each field defaults to `true`, meaning your account's tracking settings apply; set a field to `false` to disable that tracking for this send only. These fields can only opt out; they cannot enable tracking that is disabled for your account.
                  properties:
                    clickTracking:
                      type: boolean
                      default: true
                      description: Set `false` to skip link rewriting so the original URLs are delivered unchanged. Useful when the click-tracking redirect domain breaks iOS/Android universal links or deep links in transactional emails.
                    openTracking:
                      type: boolean
                      default: true
                      description: Set `false` to skip the open-tracking pixel for this send only.
                  example:
                    clickTracking: false
            examples:
              slug-based:
                summary: Send using template slug
                value:
                  to: "recipient@example.com"
                  slug: "welcome-email"
                  variables:
                    NAME: "John"
              repeat-array-data:
                summary: Send array data to a repeat block
                value:
                  to: "recipient@example.com"
                  slug: "order-confirmation"
                  variables:
                    NAME: "John"
                    event:
                      items:
                        - title: "Pro plan"
                          description: "Monthly subscription"
                        - title: "Priority support"
                          description: "Account add-on"
              direct-content:
                summary: Send with direct content
                value:
                  to: "recipient@example.com"
                  subject: "Your order confirmation"
                  body: "<h1>Order Confirmed</h1><p>Thank you for your order, {{NAME}}!</p>"
                  preview: "Your order has been confirmed"
                  variables:
                    NAME: "John"
              compatibility-aliases:
                summary: Send direct content using the HTML alias
                value:
                  to: "recipient@example.com"
                  subject: "Connection test"
                  html: "<p>Connected.</p>"
              with-custom-from-and-reply:
                summary: Send with custom from and reply-to
                value:
                  to: "recipient@example.com"
                  slug: "order-confirmation"
                  variables:
                    NAME: "John"
                    ORDER_ID: "12345"
                  from: "Notifications <notifications@mycompany.com>"
                  replyTo: "Support <support@mycompany.com>"
              multiple-recipients:
                summary: Send to multiple recipients
                value:
                  to:
                    - "user1@example.com"
                    - "user2@example.com"
                  slug: "order-confirmation"
                  variables:
                    ORDER_ID: "12345"
              with-attachments:
                summary: Send with file attachments
                value:
                  to: "customer@example.com"
                  slug: "invoice"
                  variables:
                    INVOICE_NUMBER: "INV-2025-001"
                  attachments:
                    - filename: "invoice.pdf"
                      path: "https://example.com/invoices/INV-2025-001.pdf"
                    - filename: "terms.pdf"
                      content: "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC..."
      responses:
        "200":
          description: Email queued successfully
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    title: Slug-based response
                    properties:
                      success:
                        type: boolean
                        example: true
                      emailSendId:
                        type: string
                        example: "send_abc123"
                        description: Durable email delivery ID. Use this with GET /email-sends/{emailSendId}.
                      emailType:
                        type: string
                        enum: [marketing, transactional]
                        description: Delivery policy accepted for the queued email.
                      jobId:
                        type: string
                        example: "job_abc123"
                        deprecated: true
                        description: Legacy queue identifier retained for response compatibility.
                      idempotentReplay:
                        type: boolean
                        description: True when this response replays an earlier request with the same Idempotency-Key.
                      to:
                        oneOf:
                          - type: string
                          - type: array
                            items:
                              type: string
                        example: "recipient@example.com"
                      transactional:
                        type: object
                        properties:
                          id:
                            type: string
                          slug:
                            type: string
                          name:
                            type: string
                      diagnostics:
                        $ref: "#/components/schemas/TransactionalSendDiagnostics"
                  - type: object
                    title: Direct content response
                    properties:
                      success:
                        type: boolean
                        example: true
                      emailSendId:
                        type: string
                        example: "send_abc123"
                        description: Durable email delivery ID. Use this with GET /email-sends/{emailSendId}.
                      emailType:
                        type: string
                        enum: [marketing, transactional]
                        description: Delivery policy accepted for the queued email.
                      jobId:
                        type: string
                        example: "job_abc123"
                        deprecated: true
                        description: Legacy queue identifier retained for response compatibility.
                      idempotentReplay:
                        type: boolean
                        description: True when this response replays an earlier request with the same Idempotency-Key.
                      to:
                        oneOf:
                          - type: string
                          - type: array
                            items:
                              type: string
                        example: "recipient@example.com"
                      diagnostics:
                        $ref: "#/components/schemas/TransactionalSendDiagnostics"
        "400":
          description: Bad request (validation error, disabled template, attachment errors, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing-params:
                  summary: Missing required parameters
                  value:
                    success: false
                    error: "Either 'slug'/'templateId' OR both 'subject' and 'body'/'html' are required"
                disabled:
                  summary: Template is disabled
                  value:
                    success: false
                    error: 'Transactional email "welcome-email" is disabled'
                no-sender:
                  summary: No sender profile configured
                  value:
                    success: false
                    error: "No sender profile configured. Please create a sender profile first."
                too-many-recipients:
                  summary: Too many recipients
                  value:
                    success: false
                    error: "Too many recipients. Maximum 50 recipients allowed."
                attachments-too-large:
                  summary: Attachments exceed size limit
                  value:
                    success: false
                    error: "Total attachment size exceeds 7MB limit."
                invalid-attachment:
                  summary: Invalid attachment format
                  value:
                    success: false
                    error: "Attachment must have either 'content' or 'path', not both."
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Idempotency-Key was already used with different request content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: Idempotency-Key was already used with a different request
                code: IDEMPOTENCY_KEY_REUSED
        "429":
          description: API key rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until the request can be retried.
            RateLimit-Limit:
              schema:
                type: integer
              description: Per-minute request limit.
            RateLimit-Remaining:
              schema:
                type: integer
              description: Remaining requests in the current minute policy.
            RateLimit-Reset:
              schema:
                type: integer
              description: Seconds until the minute budget resets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Send was reserved but queue infrastructure is temporarily unavailable; retry with the same Idempotency-Key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: Email send is reserved but could not be queued; retry with the same Idempotency-Key
                code: TRANSACTIONAL_ENQUEUE_UNAVAILABLE
                retryable: true
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                success: false
                error: 'Transactional email with slug "unknown-template" not found'
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Forms Operations
  # ============================================================================
  /forms:
    get:
      summary: List saved forms
      description: Lists saved signup forms for the authenticated workspace, including their server-managed audience settings and public action URLs.
      operationId: listSavedForms
      tags:
        - Widgets
      responses:
        "200":
          description: Saved forms returned successfully
          content:
            application/json:
              schema:
                type: object
                required: [success, companyId, forms]
                properties:
                  success:
                    type: boolean
                    example: true
                  companyId:
                    type: string
                  forms:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        status:
                          type: string
                        settings:
                          type: object
                          additionalProperties: true
                        actionUrl:
                          type: string
                          format: uri
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create saved form
      description: Creates and publishes a saved signup form. Its opaque form ID becomes a client-safe public capability while audience and success settings remain server-side.
      operationId: createSavedForm
      tags:
        - Widgets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, listIds]
              properties:
                name:
                  type: string
                  maxLength: 255
                listIds:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: string
                tagIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                buttonText:
                  type: string
                headline:
                  type: string
                description:
                  type: string
                successMessage:
                  type: string
                redirectUrl:
                  type: string
                  format: uri
                showFirstName:
                  type: boolean
                showLastName:
                  type: boolean
                theme:
                  type: object
                  description: Optional visual theme overrides (accentColor, backgroundColor, textColor, mutedTextColor, cardColor, borderColor as "#rrggbb", borderRadius 0-32, headingFontFamily, bodyFontFamily, density).
                  additionalProperties: true
      responses:
        "200":
          description: Form created and published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  form:
                    type: object
                    additionalProperties: true
                  embed:
                    type: object
                    additionalProperties: true
        "400":
          description: Invalid audience, duplicate strategy, or redirect URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to create form
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /forms/embed/{formId}:
    get:
      summary: Get saved form embed recipes
      description: Returns a published saved form's public action URL, hosted JavaScript, minimal native form, fetch enhancement, and supported static-site platforms.
      operationId: getSavedFormEmbed
      tags:
        - Widgets
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Embed recipes returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  form:
                    type: object
                    additionalProperties: true
                  embed:
                    type: object
                    properties:
                      actionUrl:
                        type: string
                        format: uri
                      scriptUrl:
                        type: string
                        format: uri
                      javascript:
                        type: string
                      nativeForm:
                        type: string
                      fetch:
                        type: string
                      supportedPlatforms:
                        type: array
                        items:
                          type: string
        "400":
          description: Form is not published
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Form not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /popups:
    get:
      summary: List saved popups
      description: Lists saved on-site signup popups for the authenticated workspace, including their trigger, targeting, audience settings, and view and conversion counts.
      operationId: listSavedPopups
      tags:
        - Widgets
      parameters:
        - name: includeContent
          in: query
          required: false
          description: Set to `true` to include every popup's full content blocks. Omitted by default because each popup adds roughly 1.8k characters; read one popup with `GET /popups/{popupId}` instead.
          schema:
            type: string
            enum: ["true", "false"]
      responses:
        "200":
          description: Saved popups returned successfully
          content:
            application/json:
              schema:
                type: object
                required: [success, companyId, popups]
                properties:
                  success:
                    type: boolean
                    example: true
                  companyId:
                    type: string
                  popups:
                    type: array
                    items:
                      $ref: "#/components/schemas/SavedPopup"
                  note:
                    type: string
                    description: Present when content blocks were omitted, explaining how to fetch them.
                  url:
                    type: string
                    format: uri
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Create saved popup
      description: |
        Creates a saved on-site signup popup and returns the one-line script tag that deploys it. The popup is published by default, so the script is live as soon as it is added to the site. Trigger, targeting, audience, and duplicate handling stay server-side, so the deployed script carries no API key.

        Omit `listIds` to capture into every list, matching the dashboard default.
      operationId: createSavedPopup
      tags:
        - Widgets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
                template:
                  type: string
                  enum:
                    - newsletter-modal
                    - discount-offer
                    - countdown-launch
                    - minimal-slide-in
                    - exit-lead-magnet
                    - live-demo
                    - launch-modal
                    - paper-digest
                    - stark-takeover
                    - top-bar
                    - announcement-bar
                    - fullscreen-welcome
                  default: newsletter-modal
                  description: Starting design for the popup's blocks and theme.
                status:
                  type: string
                  enum: [draft, published]
                  default: published
                listIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                  description: Lists every signup is added to. Omit or pass an empty array to capture into every list.
                tagIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                headline:
                  type: string
                description:
                  type: string
                buttonText:
                  type: string
                successMessage:
                  type: string
                redirectUrl:
                  type: string
                  format: uri
                presentation:
                  type: string
                  enum: [modal, slide-in, floating-bar, fullscreen]
                placement:
                  type: string
                  enum: [center, left, right, top, bottom]
                trigger:
                  $ref: "#/components/schemas/SavedPopupTrigger"
                targeting:
                  $ref: "#/components/schemas/SavedPopupTargeting"
                schedule:
                  $ref: "#/components/schemas/SavedPopupSchedule"
                frequency:
                  $ref: "#/components/schemas/SavedPopupFrequency"
                visual:
                  $ref: "#/components/schemas/SavedPopupVisual"
                theme:
                  type: object
                  description: Optional visual theme overrides (accentColor, backgroundColor, textColor, mutedTextColor, cardColor, borderColor as "#rrggbb", borderRadius 0-32, headingFontFamily, bodyFontFamily, density).
                  additionalProperties: true
                blocks:
                  type: array
                  maxItems: 200
                  description: Complete replacement for the popup's content blocks. The popup must keep exactly one required email field and one submit button.
                  items:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: Popup created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popup:
                    $ref: "#/components/schemas/SavedPopup"
                  embed:
                    $ref: "#/components/schemas/SavedPopupEmbed"
                  message:
                    type: string
        "400":
          description: Invalid template, status, audience, redirect URL, or popup content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to create popup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /popups/embed/{popupId}:
    get:
      summary: Get saved popup embed recipes
      description: Returns a published popup's script URL plus ready-to-paste snippets for plain HTML, React and Next.js, WordPress, and Shopify. The snippets carry no API key.
      operationId: getSavedPopupEmbed
      tags:
        - Widgets
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Embed recipes returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popup:
                    $ref: "#/components/schemas/SavedPopup"
                  embed:
                    $ref: "#/components/schemas/SavedPopupEmbed"
        "400":
          description: Popup is not published
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Popup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /popups/{popupId}/duplicate:
    post:
      summary: Duplicate saved popup
      description: Copies a saved popup into a new draft with its own view and conversion counts. The original keeps its status and stats, so a live popup carries on showing while the copy is edited.
      operationId: duplicateSavedPopup
      tags:
        - Widgets
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Name for the copy. Defaults to the original name with " (copy)" appended.
      responses:
        "200":
          description: Popup duplicated as a draft
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popup:
                    $ref: "#/components/schemas/SavedPopup"
                  message:
                    type: string
        "400":
          description: Invalid name
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Popup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to duplicate popup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /popups/{popupId}:
    get:
      summary: Get saved popup
      description: Returns one saved popup with its complete content blocks, trigger, targeting, schedule, frequency, and theme. Read this before replacing blocks so the replacement array stays complete.
      operationId: getSavedPopup
      tags:
        - Widgets
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Popup returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popup:
                    $ref: "#/components/schemas/SavedPopup"
                  embed:
                    $ref: "#/components/schemas/SavedPopupEmbed"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:read
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Popup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      summary: Update saved popup
      description: |
        Updates a saved popup. Only the fields you send change.

        Set `status` to `published` to make the popup live, or `draft` to stop it showing while keeping the popup, its stats, and its embed script. `trigger`, `targeting`, `schedule`, `frequency`, and `visual` are merged key by key, so patching one key keeps the rest.
      operationId: updateSavedPopup
      tags:
        - Widgets
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                status:
                  type: string
                  enum: [draft, published]
                listIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                  description: Replacement list targeting. Pass an empty array to capture into every list.
                tagIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                  description: Replacement tag IDs. Pass an empty array to clear tags.
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                headline:
                  type: string
                  description: New text for the popup's first heading block. Fails when the popup has no heading block.
                description:
                  type: string
                  description: New text for the popup's first paragraph block. Fails when the popup has no paragraph block.
                buttonText:
                  type: string
                successMessage:
                  type: string
                redirectUrl:
                  type: string
                  description: HTTP or HTTPS URL for successful signups. Pass an empty string to switch back to the confirmation message.
                presentation:
                  type: string
                  enum: [modal, slide-in, floating-bar, fullscreen]
                placement:
                  type: string
                  enum: [center, left, right, top, bottom]
                trigger:
                  $ref: "#/components/schemas/SavedPopupTrigger"
                targeting:
                  $ref: "#/components/schemas/SavedPopupTargeting"
                schedule:
                  $ref: "#/components/schemas/SavedPopupSchedule"
                frequency:
                  $ref: "#/components/schemas/SavedPopupFrequency"
                visual:
                  $ref: "#/components/schemas/SavedPopupVisual"
                theme:
                  type: object
                  description: Visual theme overrides merged into the current theme.
                  additionalProperties: true
                blocks:
                  type: array
                  maxItems: 200
                  description: Complete replacement for the popup's content blocks. The popup must keep exactly one required email field and one submit button.
                  items:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: Popup updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popup:
                    $ref: "#/components/schemas/SavedPopup"
                  embed:
                    $ref: "#/components/schemas/SavedPopupEmbed"
                  message:
                    type: string
        "400":
          description: Invalid status, audience, redirect URL, or popup content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Popup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to update popup
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      summary: Delete saved popup
      description: Permanently deletes a saved popup along with its view and conversion counts. Subscribers it already captured are not affected. To stop a popup from showing while keeping its stats, set its status to draft instead.
      operationId: deleteSavedPopup
      tags:
        - Widgets
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Popup deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  popupId:
                    type: string
                  message:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:delete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Popup not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /forms/{companyIdOrFormId}:
    patch:
      summary: Update saved form
      description: |
        Update a saved form's name, audience targeting, copy, visual theme, or content blocks. Every field is optional - send only what should change.

        The `headline`, `description`, `buttonText`, and `successMessage` fields edit the matching content block and fail with 400 when the form has no such block; replace `blocks` for structural changes. The `blocks` array fully replaces the form's content blocks and must keep exactly one required email field and one submit button. An empty `redirectUrl` switches the form back to its confirmation message.

        Blocks render in array order and each needs a unique `id` and a `kind`. Input blocks use `kind: "form-field"` with `fieldType` (text, email, phone, number, textarea, select, radio, checkbox, consent, hidden), `name` (the custom attribute key), `label`, `placeholder`, `required`, `defaultValue`, `showLabel`, `width` (full or half), `mapsTo` (email, firstName, lastName, phone, customAttribute; defaults to customAttribute), and `options` for choice fields (`[{ value, label, id }]`, where label and id default to value). A hidden field with a `defaultValue` stores that server-owned value and ignores submitted values; a hidden field without one stores the value the page submits. Validation errors name the offending property, for example `blocks[3].options[0].value`.
      operationId: updateSavedForm
      tags:
        - Widgets
      parameters:
        - name: companyIdOrFormId
          in: path
          required: true
          schema:
            type: string
          description: The saved form ID to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                listIds:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: string
                tagIds:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                  description: Replacement tag IDs. An empty array clears tags.
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                headline:
                  type: string
                description:
                  type: string
                buttonText:
                  type: string
                successMessage:
                  type: string
                redirectUrl:
                  type: string
                  description: HTTP or HTTPS success redirect. An empty string switches back to the confirmation message.
                theme:
                  type: object
                  description: Visual theme overrides merged into the current theme (accentColor, backgroundColor, textColor, mutedTextColor, cardColor, borderColor as "#rrggbb", borderRadius 0-32, headingFontFamily, bodyFontFamily, density).
                  additionalProperties: true
                blocks:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    additionalProperties: true
                  description: Full replacement for the form's content blocks.
      responses:
        "200":
          description: Form updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  form:
                    type: object
                    additionalProperties: true
                  embed:
                    type: object
                    additionalProperties: true
                    description: Embed recipes, present when the form is published.
                  message:
                    type: string
        "400":
          description: Invalid audience, theme, copy target, or content blocks
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing widgets:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Form not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to update form
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      summary: Submit signup form
      description: |
        Submit a public signup form. No API key is required.

        When the path value is a saved form ID, the form's stored settings are used for audience targeting and success behavior. When the path value is a company ID, this endpoint uses the legacy company-level form behavior.

        Omit `lists` to use the workspace default lists setting, provide `lists=` to add the subscriber to no lists, or provide comma-separated list IDs for specific lists. Provide stable `tags` IDs to apply existing tags to the subscriber.
      operationId: submitSignupForm
      tags:
        - Widgets
      security: []
      parameters:
        - name: companyIdOrFormId
          in: path
          required: true
          schema:
            type: string
          description: A saved form ID, or a company ID for legacy generated forms
        - name: lists
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list IDs. Omit for workspace default lists, or provide an empty value for no lists.
          example: list_abc123,list_def456
        - name: tags
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated tag IDs to apply to the subscriber.
          example: tag_abc123,tag_def456
        - name: duplicateStrategy
          in: query
          required: false
          schema:
            type: string
            enum: [skip, merge, overwrite]
            default: skip
          description: How to handle an existing contact with the submitted email. Use skip to preserve fields, merge to fill missing fields, or overwrite to replace submitted fields. Merge and overwrite require duplicateStrategyToken from the form builder.
          example: merge
        - name: duplicateStrategyToken
          in: query
          required: false
          schema:
            type: string
          description: Signed token generated by the form builder for the selected duplicateStrategy. Required for merge or overwrite.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                firstName:
                  type: string
                  description: Subscriber first name
                lastName:
                  type: string
                  description: Subscriber last name
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format. Stored on the base subscriber profile and does not grant SMS consent.
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes. HTML forms can submit individual attributes with bracketed field names such as customAttributes[nickname].
                "listIds[]":
                  type: array
                  items:
                    type: string
                  description: Existing deployed embed format for repeated list IDs
                "tagIds[]":
                  type: array
                  items:
                    type: string
                  description: Existing deployed embed format for repeated tag IDs
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                  description: Body alternative to the duplicateStrategy query parameter. Query parameter takes precedence. Merge and overwrite require duplicateStrategyToken.
                duplicateStrategyToken:
                  type: string
                  description: Body alternative to the duplicateStrategyToken query parameter.
                redirectUrl:
                  type: string
                  description: Http(s) URL or bare domain to redirect to after successful submission
                  example: https://example.com/thank-you
                website:
                  type: string
                  description: Honeypot field. Leave empty.
            encoding:
              "listIds[]":
                style: form
                explode: true
              "tagIds[]":
                style: form
                explode: true
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                  example: user@example.com
                firstName:
                  type: string
                  example: Jane
                lastName:
                  type: string
                  example: Doe
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format. Stored on the base subscriber profile and does not grant SMS consent.
                  example: "+12133734253"
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes
                  example:
                    nickname: Ace
                    zipcode: "19104"
                listIds:
                  type: array
                  items:
                    type: string
                  example: ["list_abc123"]
                tagIds:
                  type: array
                  items:
                    type: string
                  description: Existing tag IDs to apply to the subscriber
                  example: ["tag_abc123", "tag_def456"]
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  default: skip
                  description: Body alternative to the duplicateStrategy query parameter. Query parameter takes precedence. Merge and overwrite require duplicateStrategyToken.
                duplicateStrategyToken:
                  type: string
                  description: Body alternative to the duplicateStrategyToken query parameter.
                redirectUrl:
                  type: string
                  description: Http(s) URL or bare domain to redirect to after successful submission
                  example: https://example.com/thank-you
                website:
                  type: string
                  description: Honeypot field. Leave empty.
      responses:
        "200":
          description: Form submitted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "303":
          description: Redirect after successful form submission
          headers:
            Location:
              description: Configured redirect URL
              schema:
                type: string
                format: uri
        "400":
          description: Invalid email, invalid redirect URL, or blocked address
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Please enter a valid email address
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Invalid form configuration
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Too many requests. Please try again later.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: An error occurred. Please try again.

  /forms/{companyIdOrFormId}/embed.js:
    get:
      summary: Get saved signup form embed script
      description: |
        Load a saved signup form with one line of JavaScript. No API key is required.

        The script renders the current saved form settings when the page loads, so dashboard edits apply to deployed JavaScript embeds without copying new HTML.
      operationId: getSavedSignupFormEmbedScript
      tags:
        - Widgets
      security: []
      parameters:
        - name: companyIdOrFormId
          in: path
          required: true
          schema:
            type: string
          description: The saved form ID
      responses:
        "200":
          description: JavaScript embed runtime for the saved form
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var currentScript = document.currentScript;
                })();
        "404":
          description: Form or company not found
          content:
            application/javascript:
              schema:
                type: string
              example: console.warn('Invalid Sequenzy form configuration.');

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /forms/{companyIdOrFormId}/{formId}/embed.js:
    get:
      summary: Get saved signup form embed script by company
      description: |
        Compatibility path for saved signup form embed scripts. New embeds should use `/forms/{formId}/embed.js`.

        The script renders the current saved form settings when the page loads, so dashboard edits apply to deployed JavaScript embeds without copying new HTML.
      operationId: getCompanyScopedSavedSignupFormEmbedScript
      tags:
        - Widgets
      security: []
      parameters:
        - name: companyIdOrFormId
          in: path
          required: true
          schema:
            type: string
          description: The company ID the form belongs to
        - name: formId
          in: path
          required: true
          schema:
            type: string
          description: The saved form ID
      responses:
        "200":
          description: JavaScript embed runtime for the saved form
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  var currentScript = document.currentScript;
                })();
        "404":
          description: Form or company not found
          content:
            application/javascript:
              schema:
                type: string
              example: console.warn('Invalid Sequenzy form configuration.');

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /forms/{companyIdOrFormId}/{formId}:
    post:
      summary: Submit saved signup form by company
      description: |
        Compatibility path for saved signup form submissions. New embeds should use `/forms/{formId}`.

        The form's stored settings are the source of truth for audience targeting (lists, tags) and success behavior (success message or redirect URL), so dashboard edits apply to deployed embeds without re-embedding. List, tag, and redirect values in the request are ignored.
      operationId: submitCompanyScopedSavedSignupForm
      tags:
        - Widgets
      security: []
      parameters:
        - name: companyIdOrFormId
          in: path
          required: true
          schema:
            type: string
          description: The company ID the form belongs to
        - name: formId
          in: path
          required: true
          schema:
            type: string
          description: The saved form ID
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                firstName:
                  type: string
                  description: Subscriber first name
                lastName:
                  type: string
                  description: Subscriber last name
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format, when configured on the saved form. Stored on the base subscriber profile and does not grant SMS consent.
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes for custom fields configured on the saved form. HTML forms can submit individual attributes with bracketed field names such as customAttributes[nickname].
                "listIds[]":
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                "tagIds[]":
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  description: Ignored for saved forms. Stored form settings are used.
                duplicateStrategyToken:
                  type: string
                  description: Ignored for saved forms. Stored form settings are used.
                redirectUrl:
                  type: string
                  description: Ignored for saved forms. Stored form settings are used.
                website:
                  type: string
                  description: Honeypot field. Leave empty.
            encoding:
              "listIds[]":
                style: form
                explode: true
              "tagIds[]":
                style: form
                explode: true
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                  example: user@example.com
                firstName:
                  type: string
                  example: Jane
                lastName:
                  type: string
                  example: Doe
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format, when configured on the saved form. Stored on the base subscriber profile and does not grant SMS consent.
                  example: "+12133734253"
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes for custom fields configured on the saved form
                  example:
                    nickname: Ace
                    favorite_fruit: Mango
                listIds:
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                "listIds[]":
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                tagIds:
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                "tagIds[]":
                  type: array
                  items:
                    type: string
                  description: Ignored for saved forms. Stored form settings are used.
                duplicateStrategy:
                  type: string
                  enum: [skip, merge, overwrite]
                  description: Ignored for saved forms. Stored form settings are used.
                duplicateStrategyToken:
                  type: string
                  description: Ignored for saved forms. Stored form settings are used.
                redirectUrl:
                  type: string
                  description: Ignored for saved forms. Stored form settings are used.
                website:
                  type: string
                  description: Honeypot field. Leave empty.
      responses:
        "200":
          description: Form submitted successfully. Native HTML form posts that accept `text/html` receive a hosted confirmation page showing the form's success message instead of JSON.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
            text/html:
              schema:
                type: string
                description: Hosted confirmation page for native HTML form posts
        "303":
          description: Redirect after successful form submission, using the redirect URL stored in the form's settings
          headers:
            Location:
              description: Configured redirect URL
              schema:
                type: string
                format: uri
        "400":
          description: Invalid email or blocked address
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Please enter a valid email address
        "404":
          description: Form not found for this company
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Invalid form configuration
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Too many requests. Please try again later.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: An error occurred. Please try again.

  /forms/popups/{popupId}:
    post:
      summary: Submit saved popup
      description: |
        Submit a saved popup without an API key. The popup's stored content is the source of truth for audience targeting, duplicate handling, custom fields, and success behavior.
      operationId: submitSavedPopup
      tags:
        - Widgets
      security: []
      parameters:
        - name: popupId
          in: path
          required: true
          schema:
            type: string
          description: The saved popup ID
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                firstName:
                  type: string
                  description: Subscriber first name, when configured on the popup
                lastName:
                  type: string
                  description: Subscriber last name, when configured on the popup
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format, when configured on the popup. Stored on the base subscriber profile and does not grant SMS consent.
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes for custom fields configured on the popup. HTML forms can submit individual attributes with bracketed field names such as customAttributes[company_size].
                website:
                  type: string
                  description: Honeypot field. Leave empty.
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: Subscriber email address
                  example: user@example.com
                firstName:
                  type: string
                  example: Jane
                lastName:
                  type: string
                  example: Doe
                phone:
                  type: string
                  description: Subscriber phone number in E.164 or US national format, when configured on the popup. Stored on the base subscriber profile and does not grant SMS consent.
                  example: "+12133734253"
                customAttributes:
                  type: object
                  additionalProperties: true
                  description: Subscriber custom attributes for custom fields configured on the popup
                  example:
                    company_size: "42"
                website:
                  type: string
                  description: Honeypot field. Leave empty.
      responses:
        "200":
          description: Popup submitted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  redirectUrl:
                    type: string
                    format: uri
                    description: Present when the popup is configured to redirect after submission
        "400":
          description: Invalid email, required field missing, or blocked address
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Please enter a valid email address
        "404":
          description: Popup not found or not published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Invalid popup configuration
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Too many requests. Please try again later.
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: An error occurred. Please try again.

  # ============================================================================
  # Widgets Operations
  # ============================================================================
  /widgets/popup.js:
    get:
      summary: Get popup widget runtime
      description: Load the hosted JavaScript runtime for popup signup widgets. No API key is required.
      operationId: getPopupWidgetRuntime
      tags:
        - Widgets
      security: []
      responses:
        "200":
          description: JavaScript runtime
          content:
            application/javascript:
              schema:
                type: string
              example: |
                (function () {
                  "use strict";
                })();

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /widgets/preferences/token:
    post:
      summary: Get preferences widget token
      description: |
        Generate a signed token to embed the subscription preferences widget for a subscriber.
        This token allows users to manage their email subscription preferences directly from your website.

        **Important:** Call this endpoint from your backend only. Never expose your API key to the frontend.
      operationId: getPreferencesToken
      tags:
        - Widgets
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                  description: The subscriber's email address
                  example: "user@example.com"
      responses:
        "200":
          description: Token generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  token:
                    type: string
                    description: Signed JWT token (valid for 1 hour)
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        "400":
          description: Invalid email format
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: "VALIDATION_ERROR"
                      message:
                        type: string
                        example: "Invalid email format"
        "401":
          description: Unauthorized - Invalid or missing API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: "UNAUTHORIZED"
                      message:
                        type: string
                        example: "Invalid API key"
        "404":
          description: Subscriber not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: "NOT_FOUND"
                      message:
                        type: string
                        example: "Subscriber not found"
        "500":
          description: Server configuration error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Subscribers - Additional Endpoints
  # ============================================================================
  /subscribers/me:
    get:
      summary: Get account info
      description: Returns account information for the authenticated API key. Useful for connection labels in integrations.
      operationId: getAccountInfo
      tags:
        - Subscribers
      responses:
        "200":
          description: Account info retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  account:
                    type: object
                    properties:
                      companyId:
                        type: string
                        example: "company_abc123"
                      companyName:
                        type: string
                        example: "Acme Inc"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Account & Setup
  # ============================================================================
  /account:
    get:
      operationId: getAccount
      summary: Get account
      description: Returns the companies available to the authenticated API key, the currently selected company, and a read-only summary of the key's own permissions.
      tags:
        - Account
      responses:
        "200":
          description: Account context returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  user:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "user_abc123"
                  companies:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        role:
                          type: string
                          description: Account-key access role. Viewer access remains read-only regardless of key scopes.
                          enum: [owner, admin, viewer]
                  currentCompanyId:
                    type: string
                    nullable: true
                  apiKeyPermissions:
                    type: object
                    description: Read-only identity and permission metadata for the authenticated key, including a recovery URL. This does not bypass resource scopes.
                    properties:
                      activeKey:
                        type: object
                        description: Non-secret identity of the bearer key that authenticated this request. Use it to verify that a restarted client loaded the intended replacement key.
                        properties:
                          id:
                            type: string
                          name:
                            type: string
                            nullable: true
                          prefix:
                            type: string
                            nullable: true
                            description: Stored non-secret key prefix, such as seq_live_A or seq_user_B. Pair it with the key name and ID when verifying a replacement credential.
                          type:
                            type: string
                            nullable: true
                            enum:
                              - company
                              - personal
                      preset:
                        type: string
                        enum:
                          - full_access
                          - read_only
                          - agent_safe
                          - ai_drafting
                          - data_ingest_safe
                          - data_ingest_automations
                          - transactional_sender
                          - marketing_sender
                          - custom
                      fullAccess:
                        type: boolean
                      selectedScopeCount:
                        type: integer
                        description: Number of currently defined scopes enabled for the key. For full-access keys this equals currentScopeCount, while fullAccess still indicates future-scope access.
                      currentScopeCount:
                        type: integer
                        description: Total number of permission scopes defined when the response was generated.
                      description:
                        type: string
                        description: Human-readable explanation of the effective permission selection.
                      scopes:
                        type: array
                        nullable: true
                        items:
                          type: string
                      canDiscoverMarketingWork:
                        type: boolean
                        description: Whether the key can read campaigns, sequences, and landing pages.
                      missingMarketingReadScopes:
                        type: array
                        items:
                          type: string
                      manageUrl:
                        type: string
                        format: uri
                        nullable: true
                        description: Direct management URL for the authenticated key. Personal keys open Account API Keys; company keys open the selected workspace's API Keys settings.
                  message:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /api-keys:
    get:
      operationId: listApiKeys
      summary: List API keys
      description: Lists company-scoped API keys as non-secret metadata. The caller must have the `api_keys:manage` permission. Account-scoped keys select the company with the x-company-id header. Plain key values and stored hashes are never returned.
      tags:
        - Account
      responses:
        "200":
          description: API key metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  apiKeys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKeyMetadata"
                required:
                  - success
                  - apiKeys
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key management permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createApiKey
      summary: Create API key
      description: Creates a company-scoped API key. The caller must have the `api_keys:manage` permission. Account-scoped keys select the target company with the x-company-id header; companyId in the JSON body is not a supported selector. The plain key is returned only once.
      tags:
        - Account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  description: Human-readable key name.
                preset:
                  type: string
                  description: Permission preset to apply when scopes is omitted. Defaults to full_access. Full-access keys are stored with scopes set to null, meaning all current and future permissions.
                  enum:
                    - full_access
                    - read_only
                    - agent_safe
                    - ai_drafting
                    - data_ingest_safe
                    - data_ingest_automations
                    - transactional_sender
                    - marketing_sender
                scopes:
                  type: array
                  description: Explicit permission scopes for the new key. Overrides preset when provided.
                  minItems: 1
                  items:
                    type: string
      responses:
        "200":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  apiKey:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      key:
                        type: string
                        description: Plain API key. This is shown only on creation.
                      prefix:
                        type: string
                      scopes:
                        type:
                          - array
                          - "null"
                        description: Explicit permission scopes, or null for full access.
                        items:
                          type: string
                      permissions:
                        type: object
                        description: Effective permission receipt for the newly created key. Only scopes set to null grant current and future full access.
                        properties:
                          preset:
                            type: string
                            description: Effective preset derived from the stored scopes. An explicit all-current-scopes array is custom, not full_access.
                          fullAccess:
                            type: boolean
                          selectedScopeCount:
                            type: integer
                          currentScopeCount:
                            type: integer
                          description:
                            type: string
                      createdAt:
                        type: string
                        format: date-time
                  message:
                    type: string
                  instructions:
                    type: object
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "400":
          description: Invalid permission preset or scope list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key management permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: API key could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api-key-handoff:
    post:
      operationId: requestApiKeyHandoff
      summary: Request API key handoff
      description: Builds a dashboard link that opens the create-key form prefilled with a suggested name and permissions. Requires only `account:read`, because it creates nothing, changes nothing, and returns no secret - the new key is issued in the owner's authenticated browser session. Use it when key management is blocked because the calling key lacks `api_keys:manage`, which cannot be granted through the API by the key that is missing it. Pass replaceApiKeyId to rotate; the dashboard then offers to revoke the predecessor once the replacement exists.
      tags:
        - Account
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  description: Suggested name for the new key. Trimmed to 80 characters in the link.
                preset:
                  type: string
                  description: Suggested permission preset.
                  enum:
                    - full_access
                    - read_only
                    - agent_safe
                    - ai_drafting
                    - data_ingest_safe
                    - data_ingest_automations
                    - transactional_sender
                    - marketing_sender
                scopes:
                  type: array
                  description: Suggested explicit permission scopes. Overrides preset when provided.
                  minItems: 1
                  items:
                    type: string
                replaceApiKeyId:
                  type: string
                  description: ID of the key the new one replaces. Pass the literal string "current" for the key making the request.
      responses:
        "200":
          description: Handoff link prepared
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  handoff:
                    type: object
                    properties:
                      url:
                        type: string
                        description: Dashboard URL that opens the create-key form prefilled with the request.
                      manageUrl:
                        type: string
                        description: The plain API Keys settings URL, without the prefill.
                      keyType:
                        type: string
                        description: Whether the link targets workspace or account API key settings.
                        enum:
                          - company
                          - personal
                      name:
                        type:
                          - string
                          - "null"
                      preset:
                        type:
                          - string
                          - "null"
                      scopes:
                        type:
                          - array
                          - "null"
                        items:
                          type: string
                      permissions:
                        type:
                          - object
                          - "null"
                        description: Permission receipt for the suggested selection. Null when the request suggested no preset and no scopes, because the form then opens on the dashboard default.
                        additionalProperties: true
                      replaces:
                        type:
                          - object
                          - "null"
                        description: The key being replaced. name and prefix are populated only when it is the key making the request.
                        properties:
                          id:
                            type: string
                          name:
                            type:
                              - string
                              - "null"
                          prefix:
                            type:
                              - string
                              - "null"
                          isCurrentKey:
                            type: boolean
                      canSelfServe:
                        type: boolean
                        description: True when the calling key already holds api_keys:manage and could create the replacement directly.
                      deliversKeyToCaller:
                        type: boolean
                        description: Always false. The new key is shown in the browser and never returned through this endpoint.
                  message:
                    type: string
                  nextSteps:
                    type: array
                    items:
                      type: string
                required:
                  - success
                  - handoff
        "400":
          description: Invalid permission preset, scope list, or replaceApiKeyId
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required account read permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api-keys/{apiKeyId}:
    patch:
      operationId: updateApiKey
      summary: Update API key
      description: Renames a company-scoped API key and/or replaces its permissions in place. The caller must have the `api_keys:manage` permission. The key value is unchanged. 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. The response contains non-secret metadata only.
      tags:
        - Account
      parameters:
        - name: apiKeyId
          in: path
          required: true
          description: Exact API key ID returned by the list API keys endpoint.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              description: Provide at least one of name, preset, or scopes.
              properties:
                name:
                  type: string
                  description: New human-readable key name.
                preset:
                  type: string
                  description: Replacement permission preset. Full-access keys are stored with scopes set to null, meaning all current and future permissions.
                  enum:
                    - full_access
                    - read_only
                    - agent_safe
                    - ai_drafting
                    - data_ingest_safe
                    - data_ingest_automations
                    - transactional_sender
                    - marketing_sender
                scopes:
                  type: array
                  description: Replacement explicit permission scopes. Overrides preset when provided.
                  minItems: 1
                  items:
                    type: string
      responses:
        "200":
          description: API key updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  apiKey:
                    $ref: "#/components/schemas/ApiKeyMetadata"
                  message:
                    type: string
                    example: API key permissions updated. Added permissions apply on the next retry; removed permissions may take up to five minutes to expire across API instances.
                required:
                  - success
                  - apiKey
                  - message
        "400":
          description: Nothing to update, or an invalid permission preset or scope list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key management permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: API key not found for the selected company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: revokeApiKey
      summary: Revoke API key
      description: Permanently revokes a company-scoped API key. The caller must have the `api_keys:manage` permission. The response contains non-secret metadata only.
      tags:
        - Account
      parameters:
        - name: apiKeyId
          in: path
          required: true
          description: Exact API key ID returned by the list API keys endpoint.
          schema:
            type: string
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  apiKey:
                    $ref: "#/components/schemas/ApiKeyMetadata"
                  message:
                    type: string
                    example: API key revoked successfully.
                required:
                  - success
                  - apiKey
                  - message
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key management permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: API key not found for the selected company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /integration-guide:
    post:
      operationId: getIntegrationGuide
      summary: Get integration guide
      description: Returns a framework-specific code example and implementation tip for common integration use cases.
      tags:
        - Account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                framework:
                  type: string
                  example: "nextjs"
                use_case:
                  type: string
                  example: "transactional"
      responses:
        "200":
          description: Integration guide returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  framework:
                    type: string
                  use_case:
                    type: string
                  code:
                    type: string
                  tip:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /feedback:
    post:
      operationId: submitFeedback
      summary: Submit feedback
      description: Submits product feedback about Sequenzy itself to the Sequenzy team - for example, when a workflow you or your user needed is not exposed via the API, CLI, or MCP server.
      tags:
        - Feedback
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - message
              properties:
                message:
                  type: string
                  minLength: 1
                  maxLength: 10000
                  description: The feedback itself. Be specific about what was needed and what was missing or wrong.
                  example: "There is no endpoint to bulk-delete campaigns by label."
                category:
                  type: string
                  enum: [missing_capability, bug, docs, ux, praise, other]
                  description: Feedback category. Use missing_capability when a needed workflow is not supported. Defaults to other.
                source:
                  type: string
                  enum: [api, cli, mcp]
                  description: Where the feedback was submitted from. Defaults to api.
                context:
                  type: string
                  maxLength: 10000
                  description: Optional description of what you were trying to accomplish when you hit the gap.
                userIntent:
                  type: string
                  maxLength: 5000
                  description: For bug or wrong-outcome reports - the user's request, verbatim or closely paraphrased. Omit personal data not needed to reproduce the problem.
                  example: "Move the promo send to 9am Tuesday"
                toolCalls:
                  type: array
                  maxItems: 25
                  description: For bug or wrong-outcome reports - the ordered API calls, CLI commands, or MCP tool calls that led to the problem. Summarize arguments; do not include raw subscriber data.
                  items:
                    type: object
                    additionalProperties: false
                    required:
                      - tool
                    properties:
                      tool:
                        type: string
                        minLength: 1
                        maxLength: 200
                        description: Tool, command, or endpoint name.
                        example: "update_campaign"
                      args:
                        type: string
                        maxLength: 2000
                        description: Short summary of the arguments used.
                      error:
                        type: string
                        maxLength: 2000
                        description: Error returned by this call, if any.
                expected:
                  type: string
                  maxLength: 5000
                  description: What you expected to happen.
                actual:
                  type: string
                  maxLength: 5000
                  description: What actually happened instead.
                resourceIds:
                  type: array
                  maxItems: 50
                  description: IDs of the affected resources so the team can correlate the report with server logs.
                  items:
                    type: string
                    minLength: 1
                    maxLength: 200
                  example: ["cmp_123"]
      responses:
        "200":
          description: Feedback received
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Feedback received. The Sequenzy team reviews every submission."
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Feedback could not be delivered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Company Operations
  # ============================================================================
  /companies:
    get:
      operationId: listCompanies
      summary: List companies
      description: Lists companies available to the authenticated API key.
      tags:
        - Companies
      responses:
        "200":
          description: Companies returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  companies:
                    type: array
                    items:
                      $ref: "#/components/schemas/Company"
                  currentCompanyId:
                    type: string
                    nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createCompany
      summary: Create company
      description: Creates a company workspace and queues brand processing for its website. Requires a personal account key (seq_user_...). Company-scoped keys (seq_live_... and legacy ek_... keys) are bound to a single company and are rejected with 403, because they could never access the workspace they created.
      tags:
        - Companies
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  description: Company display name. If omitted, Sequenzy derives it from the domain.
                domain:
                  type: string
                  description: Company website domain or URL.
              required:
                - domain
      responses:
        "200":
          description: Company created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  company:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      status:
                        type: string
                      websiteUrl:
                        type: string
                  message:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The API key is scoped to a single company and cannot create another one
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: This API key is scoped to a single company, so it cannot create another one. Retry with a personal account key (seq_user_...) from Account -> API Keys, or create the company in the dashboard.
                code: COMPANY_CREATE_REQUIRES_PERSONAL_KEY
        "500":
          description: Company could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /companies/{companyId}:
    get:
      operationId: getCompany
      summary: Get company
      description: Returns one company workspace that the authenticated key can access.
      tags:
        - Companies
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: string
          description: Company ID
      responses:
        "200":
          description: Company returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  company:
                    $ref: "#/components/schemas/Company"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      operationId: updateCompany
      summary: Update company
      description: Updates product info, brand context, the default email theme, reply-tracking settings, the workspace default lists, and account-wide From/Reply-To defaults. New profiles are created as needed; From addresses require a verified sending domain. Requires the company_profile:manage scope; the sending-identity, reply-tracking, and defaultSubscriberListIds fields additionally require companies:manage.
      tags:
        - Companies
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: string
          description: Company ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
                logoUrl:
                  type: string
                  nullable: true
                founderName:
                  type: string
                  nullable: true
                primaryColor:
                  type: string
                  description: 6-digit hex color, for example #0ea5e9.
                brandColors:
                  type: object
                  nullable: true
                  additionalProperties: true
                valueProps:
                  type: array
                  nullable: true
                  items:
                    type: object
                    additionalProperties: true
                testimonials:
                  type: array
                  nullable: true
                  items:
                    type: object
                    additionalProperties: true
                toneVoice:
                  type: string
                  nullable: true
                companyContext:
                  type: string
                  nullable: true
                emailDesignPrompt:
                  type: string
                  nullable: true
                  description: >-
                    Art direction for AI-designed emails: layout, density,
                    which sections belong in an email, imagery, and CTA
                    prominence. `toneVoice` steers copy; this steers design.
                    When empty, the next email generation prefills it with
                    the direction derived from the brand; null clears it so
                    the next generation writes a fresh one.
                emailLengthPreference:
                  type: string
                  enum: [concise, balanced, detailed]
                  description: >-
                    How long AI-written email copy should be. New workspaces
                    default to `concise`.
                socialLinks:
                  type: object
                  nullable: true
                  additionalProperties:
                    type: string
                privacyPolicyUrl:
                  type: string
                  nullable: true
                termsUrl:
                  type: string
                  nullable: true
                address:
                  type: string
                  nullable: true
                language:
                  type: string
                pricing:
                  type: object
                  nullable: true
                  additionalProperties: true
                fontFamily:
                  type: string
                  nullable: true
                emailTheme:
                  type: [object, "null"]
                  description: Default email theme. Partial update - omitted fields keep their current value (or the preset default) and numeric values are clamped to supported ranges. Pass null to reset to the platform default theme.
                  additionalProperties: false
                  properties:
                    presetId:
                      type: string
                      enum: [default, soft, editorial, bold]
                    buttonStyle:
                      type: string
                      enum: [solid, outline]
                      description: How primary buttons are filled. "outline" renders them as a transparent box with a brand-color border; "solid" (default) is the classic filled button.
                    colors:
                      type: object
                      description: 6-digit hex values.
                      additionalProperties: false
                      properties:
                        primary:
                          type: string
                        background:
                          type: string
                        surface:
                          type: string
                        text:
                          type: string
                        mutedText:
                          type: string
                        heading:
                          type: string
                        border:
                          type: string
                        link:
                          type: string
                        buttonText:
                          type: string
                          description: Label color for solid buttons. Omit to auto-derive a readable color from the button background.
                    typography:
                      type: object
                      description: Numeric type settings.
                      additionalProperties: false
                      properties:
                        baseFontSize:
                          type: number
                        leadFontSize:
                          type: number
                        baseLineHeight:
                          type: number
                        heading1Size:
                          type: number
                        heading2Size:
                          type: number
                        heading3Size:
                          type: number
                        buttonFontSize:
                          type: number
                        buttonFontWeight:
                          type: number
                          description: CTA label weight, clamped to 400-800.
                        headingFontWeight:
                          type: number
                          description: Heading weight applied to all heading levels, clamped to 300-900. Omit for the per-level defaults.
                        headingFontFamily:
                          type: string
                          description: Font stack for headings when it differs from the email body font. Omit so headings inherit the email font.
                        headingLetterSpacing:
                          type: number
                          description: Heading letter spacing in pixels (negative = tighter), clamped to -2..4. Omit for natural tracking.
                    layout:
                      type: object
                      description: Numeric layout settings.
                      additionalProperties: false
                      properties:
                        contentWidth:
                          type: number
                        containerPaddingX:
                          type: number
                        containerPaddingY:
                          type: number
                        blockSpacing:
                          type: number
                        baseRadius:
                          type: number
                        sectionPadding:
                          type: number
                        buttonPaddingX:
                          type: number
                        buttonPaddingY:
                          type: number
                        borderedBlockPadding:
                          type: number
                emailDirection:
                  type: string
                  enum: [ltr, rtl]
                senderProfileId:
                  type: string
                  description: Existing sender profile to make the account-wide default, and the profile fromName renames. List IDs with GET /v1/sender-profiles. Mutually exclusive with fromEmail.
                fromEmail:
                  type: string
                  format: email
                  description: Account-wide default From address. The domain must be configured and verified.
                fromName:
                  type: string
                  description: Display name of the default From profile. Sent on its own it renames the current default profile; with senderProfileId it renames that profile; with fromEmail it names the profile for that address. If the address already carries several display names, the request is rejected - pass senderProfileId to say which one to rename.
                replyProfileId:
                  type: string
                  description: Existing reply profile to make the account-wide default, and the profile replyToName renames. Mutually exclusive with replyTo.
                replyTo:
                  type: string
                  format: email
                  description: Account-wide default Reply-To address. A reply profile is created when needed.
                replyToName:
                  type: string
                  description: Display name of the default Reply-To profile. Sent on its own it renames the current default profile; with replyProfileId it renames that profile; with replyTo it names the profile for that address.
                replyTrackingEnabled:
                  type: boolean
                  description: Enable or disable inbound reply capture.
                replyTrackingDomainMode:
                  type: string
                  enum: [sequenzy, custom]
                  description: Use Sequenzy's managed inbound domain or a configured custom domain.
                forwardReplies:
                  type: boolean
                  description: Enable or disable forwarding captured replies to the configured mailbox.
                defaultSubscriberListIds:
                  type: array
                  nullable: true
                  maxItems: 100
                  items:
                    type: string
                  description: >-
                    Which lists new contacts join when something creates a
                    subscriber without explicit list targeting - forms, API
                    writes, events, tag actions, imports, and any integration
                    without its own list targeting. null means every current
                    and future list, [] means no list at all, and an array
                    means exactly those lists. Unknown or foreign list IDs are
                    rejected rather than skipped. Applies only to later
                    writes; nobody is moved or removed retroactively. Requires
                    the companies:manage scope.
      responses:
        "200":
          description: Company updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  company:
                    $ref: "#/components/schemas/Company"
        "400":
          description: Invalid update
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Sending-identity fields were sent without the companies:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Sync Rules
  # ============================================================================
  /shopify/automation-settings:
    get:
      operationId: getShopifyAutomationSettings
      summary: Get Shopify automation settings
      description: Returns the connected Shopify store's browse-abandonment, cart-abandonment, and price-drop automation settings, with platform defaults applied where the store hasn't overridden them.
      tags:
        - Shopify
      responses:
        "200":
          description: Automation settings returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  integrationId:
                    type: string
                  shopDomain:
                    type: string
                  browseAbandonment:
                    $ref: "#/components/schemas/ShopifyBrowseAbandonmentSettings"
                  cartAbandonment:
                    $ref: "#/components/schemas/ShopifyCartAbandonmentSettings"
                  priceDrop:
                    $ref: "#/components/schemas/ShopifyPriceDropSettings"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No active Shopify integration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateShopifyAutomationSettings
      summary: Update Shopify automation settings
      description: "Partial update of the store's browse-abandonment, cart-abandonment, and/or price-drop settings: omitted sections are untouched, omitted fields keep their current value, and null resets a section to the platform defaults."
      tags:
        - Shopify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                browseAbandonment:
                  nullable: true
                  allOf:
                    - $ref: "#/components/schemas/ShopifyBrowseAbandonmentSettings"
                cartAbandonment:
                  nullable: true
                  allOf:
                    - $ref: "#/components/schemas/ShopifyCartAbandonmentSettings"
                priceDrop:
                  nullable: true
                  allOf:
                    - $ref: "#/components/schemas/ShopifyPriceDropSettings"
            examples:
              raise-price-drop-threshold:
                summary: Alert only on 10%+ drops
                value:
                  priceDrop:
                    minPercent: 10
              disable-browse-abandonment:
                summary: Turn off browse abandonment
                value:
                  browseAbandonment:
                    enabled: false
              lengthen-cart-inactivity:
                summary: Wait 2h of cart inactivity
                value:
                  cartAbandonment:
                    delayHours: 2
              reset-price-drop:
                summary: Reset price drop to defaults
                value:
                  priceDrop: null
      responses:
        "200":
          description: Automation settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  integrationId:
                    type: string
                  shopDomain:
                    type: string
                  browseAbandonment:
                    $ref: "#/components/schemas/ShopifyBrowseAbandonmentSettings"
                  cartAbandonment:
                    $ref: "#/components/schemas/ShopifyCartAbandonmentSettings"
                  priceDrop:
                    $ref: "#/components/schemas/ShopifyPriceDropSettings"
        "400":
          description: No settings provided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Request body failed schema validation
          content:
            application/json:
              schema:
                type: object
                properties:
                  type:
                    type: string
                    example: "validation"
                  on:
                    type: string
                    example: "body"
                  property:
                    type: string
                    example: "/cartAbandonment/delayHours"
                  message:
                    type: string
                    example: "Expected number to be less or equal to 168"
                  summary:
                    type: string
                    example: "Property 'delayHours' should be less or equal to 168"
                  errors:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No active Shopify integration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sync-rules:
    get:
      operationId: getSyncRules
      summary: Get sync rules
      description: Returns the company's effective sync rules - the automatic tag changes applied when events fire. New companies start with an empty rule set; legacy companies may inherit the optional SaaS/ecommerce platform preset. isDefault reports whether that preset is active.
      tags:
        - Sync Rules
      responses:
        "200":
          description: Sync rules returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  syncRules:
                    type: array
                    items:
                      $ref: "#/components/schemas/SyncRule"
                  isDefault:
                    type: boolean
                    description: True while the company still uses the platform default rules.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateSyncRules
      summary: Update sync rules
      description: Replaces the company's full sync rule set. Send an empty array to disable rules, or null to opt into the inherited SaaS/ecommerce platform preset. This is not a partial update - fetch the current rules, edit them, and send the whole set back.
      tags:
        - Sync Rules
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - syncRules
              properties:
                syncRules:
                  type: array
                  nullable: true
                  description: Full replacement rule set. An empty array disables rules; null opts into the inherited SaaS/ecommerce platform preset.
                  items:
                    $ref: "#/components/schemas/SyncRule"
            examples:
              tag-buyers-by-product:
                summary: Tag buyers of a product tag
                value:
                  syncRules:
                    - triggerEvent: ecommerce.order_placed
                      actions:
                        addTags: ["vinyl-collector"]
                        removeTags: []
                      conditions:
                        purchasedProduct:
                          tags: ["Vinyl"]
              use-platform-preset:
                summary: Opt into the SaaS/ecommerce platform preset
                value:
                  syncRules: null
      responses:
        "200":
          description: Sync rules updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Saved 1 sync rule.
                  syncRules:
                    type: array
                    items:
                      $ref: "#/components/schemas/SyncRule"
                  isDefault:
                    type: boolean
        "400":
          description: Invalid rules
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Account Readiness (read-only audit surfaces)
  # ============================================================================
  /integrations:
    get:
      operationId: listIntegrations
      summary: List integrations
      description: Lists connected integrations with connection state, sync health, last sync error, and any records the last sync could not import normally. Credentials, access tokens, and webhook secrets are never returned.
      tags:
        - Integrations
      parameters:
        - name: includeInactive
          in: query
          required: false
          description: Include disconnected integrations. Defaults to false.
          schema:
            type: boolean
      responses:
        "200":
          description: Integrations returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  integrations:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationSummary"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /events/schemas:
    get:
      operationId: getEventSchemas
      summary: Get event schemas
      description: Returns the published payload of a built-in event - a real example payload per provider, plus every property path with its type, the merge tag that resolves it, and a description wherever the example alone is ambiguous (a null sample, an empty list, a unit that is not obvious, or a type that differs per provider). Omit eventName to list every documented event. Static reference data describing the shape of an event, not what the account has received. An event with no published payload returns documented false; it is still valid to trigger and to build a sequence on, because custom events carry exactly the properties you send.
      tags:
        - Events
      parameters:
        - name: eventName
          in: query
          required: false
          description: Event to describe, such as ecommerce.order_placed. Legacy aliases like order.completed resolve to their current name. Omit to list every documented event.
          schema:
            type: string
        - name: provider
          in: query
          required: false
          description: "Return only this provider's payload: shopify, woocommerce, manual, api, or stripe."
          schema:
            type: string
            enum: [shopify, woocommerce, manual, api, stripe]
      responses:
        "200":
          description: Event schemas returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  eventName:
                    type: string
                    nullable: true
                    description: Normalized name of the event described, or null when listing every documented event.
                    example: ecommerce.order_placed
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/EventSchema"
                  note:
                    type: string
                    description: Present in listing mode only.
        "400":
          description: Unknown provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /integrations/catalog:
    get:
      operationId: listIntegrationCapabilities
      summary: List integration capabilities
      description: Describes what each integration provider syncs, which events it emits and when, the subscriber attributes it writes, and which actions it supports. Works whether or not the provider is connected, so it can be used to compare providers before connecting one.
      tags:
        - Integrations
      parameters:
        - name: provider
          in: query
          required: false
          description: Return only this provider, for example stripe.
          schema:
            type: string
        - name: category
          in: query
          required: false
          description: "Filter by category: payments, ecommerce, auth, analytics, ads, affiliate, cms, or developer."
          schema:
            type: string
      responses:
        "200":
          description: Provider capabilities returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  providers:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationProviderCapability"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /integrations/connect:
    post:
      operationId: connectIntegration
      summary: Connect integration
      description: "Connects an API-key / webhook-secret integration: polar, paddle, dodo, whop, creem, chargebee, clerk, posthog, segment, or affonso. Credentials are validated against the provider where possible, stored encrypted, and never returned. Payment providers queue their initial revenue backfill; Affonso queues its affiliate backfill; PostHog and Segment can optionally import event history. The response includes the webhookUrl to configure at the provider with the same secret. Reconnecting replaces stored credentials. OAuth and app-install providers (Stripe, Shopify, Supabase, GitHub, WooCommerce, Meta) return a 400 pointing at the dashboard. Requires the integrations:manage scope."
      tags:
        - Integrations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - provider
                - webhookSecret
              properties:
                provider:
                  type: string
                  enum:
                    [
                      polar,
                      paddle,
                      dodo,
                      whop,
                      creem,
                      chargebee,
                      clerk,
                      posthog,
                      segment,
                      affonso,
                    ]
                  description: Provider to connect.
                webhookSecret:
                  type: string
                  description: "Signing secret of the webhook created at the provider. For Chargebee, the webhook's basic-auth credentials as username:password. For Segment, the secret is your own choice and must be between 16 and 153 UTF-8 bytes."
                apiKey:
                  type: string
                  description: Provider API key. Required for every provider except clerk, posthog, and segment.
                providerAccountId:
                  type: string
                  description: "Provider account id: Paddle seller ID, Dodo business ID, Whop company ID, Creem store ID, or Chargebee site name. Polar resolves it from the API key."
                settings:
                  type: object
                  description: "PostHog and Segment only. Event delivery scope. PostHog defaults to every non-internal event; new Segment connections skip automatic page/screen calls unless explicitly allowlisted."
                  properties:
                    syncAllEvents:
                      type: boolean
                    eventAllowlist:
                      type: array
                      items:
                        type: string
                historyImport:
                  type: object
                  description: "PostHog and Segment only. Imports event history after connecting: PostHog reads the project archive (projectId + personalApiKey); Segment walks your existing contacts' Unify profiles (spaceId + profileApiToken) and covers at most the last 14 days the Profile API serves, because Segment has no bulk event export."
                  required:
                    - region
                  properties:
                    region:
                      type: string
                      enum: [us, eu]
                    projectId:
                      type: string
                      description: PostHog only. Numeric PostHog project ID.
                    personalApiKey:
                      type: string
                      description: PostHog only. Personal API key with query read access.
                    spaceId:
                      type: string
                      description: Segment only. Unify space ID (spa_...).
                    profileApiToken:
                      type: string
                      description: Segment only. Profile API access token for the space.
      responses:
        "200":
          description: Integration connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  integration:
                    $ref: "#/components/schemas/IntegrationSummary"
                  webhookUrl:
                    type: string
                    description: URL to configure in the provider's webhook settings with the same secret.
                  revenueSyncQueued:
                    type: boolean
                    description: Payment providers only. Whether the initial revenue backfill was queued.
                  backfillQueued:
                    type: boolean
                    description: Affonso only. Whether the affiliate backfill was queued.
                  history:
                    type: object
                    description: PostHog and Segment only. Outcome of the optional history import request.
                    properties:
                      requested:
                        type: boolean
                      queued:
                        type: boolean
                      error:
                        type: string
                        nullable: true
        "400":
          description: Missing or invalid fields, or a provider that requires the dashboard
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: The Sequenzy API key is missing or invalid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The Sequenzy API key lacks integrations:manage or company access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: The provider account is already connected to another workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /integrations/activity:
    get:
      operationId: listIntegrationActivity
      summary: List integration activity
      description: Recent integration webhook and sync activity, newest first. Retained for 24 hours. Payloads are sanitized when written, so no credentials or signatures appear. Requires the account:read and subscribers:read scopes.
      tags:
        - Integrations
      parameters:
        - name: integrationId
          in: query
          required: false
          description: Only show activity for this integration.
          schema:
            type: string
        - name: provider
          in: query
          required: false
          description: Only show activity for this provider.
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter by activity status.
          schema:
            type: string
            enum: [received, queued, processed, skipped, failed]
        - name: limit
          in: query
          required: false
          description: Rows to return, 1-100. Defaults to 25.
          schema:
            type: integer
      responses:
        "200":
          description: Activity returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  windowHours:
                    type: integer
                    example: 24
                  note:
                    type: string
                  activity:
                    type: array
                    items:
                      $ref: "#/components/schemas/IntegrationActivityEntry"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key scope (account:read and subscribers:read are required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /integrations/{id}:
    get:
      operationId: getIntegration
      summary: Get integration
      description: Inspects one connected integration - what the provider syncs, every event it emits, the tags each event applies through the company's sync rules, the sequences that trigger on those events, recent activity, the ingestion block naming which lists its contacts join, and prioritized recommendations. Credentials are never returned. Requires the account:read, subscribers:read, sequences:read, and lists:read scopes.
      tags:
        - Integrations
      parameters:
        - name: id
          in: path
          required: true
          description: Integration ID.
          schema:
            type: string
      responses:
        "200":
          description: Integration detail returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationDetail"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Missing required API key scope (account:read, subscribers:read, sequences:read, and lists:read are required)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      # Unchanged on purpose: generated SDKs use the operationId as the method
      # name, so renaming it would delete the shipped client method even though
      # the route and its existing fields are untouched.
      operationId: updateIntegrationSync
      summary: Update integration bulk sync and list targeting
      description: "Controls what a connected integration does to the contact list. Two independent settings - `syncEnabled` turns bulk imports and backfills on or off, and `listIds` chooses which lists the contacts the provider's live webhook creates join. Neither stops that webhook: disabling bulk sync only pauses full imports, and list targeting changes membership only. Contacts are still created, their attributes still sync, sync-rule tags still apply, and default any_contact sequences still enroll them. Explicit any_list and specific-list sequences require a matching membership and do not enroll a list-less contact. `listIds` takes effect on future provider writes: nothing is applied retroactively and nobody is ever removed from a list. Wix or Webflow submissions, Shopify customer updates, and Supabase resubscriptions can add an existing contact to the new targets; Stripe applies targeting only when its webhook creates a subscriber. Provider support is declared in the catalog's `actions` as set_list_targeting. At least one field is required, an in-flight sync must finish before bulk sync can be disabled, and setting the current state succeeds with `changed: false`. Requires the integrations:manage scope."
      tags:
        - Integrations
      parameters:
        - name: id
          in: path
          required: true
          description: Integration ID.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: At least one field is required.
              properties:
                syncEnabled:
                  type: boolean
                  description: True to enable bulk imports and backfills, false to pause them. This does not stop the provider's live webhook creating contacts.
                listIds:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: "Lists that contacts created by this integration join, applied from the provider's next write onward. `null` clears the choice so they follow the workspace default lists; `[]` means they join no list; a populated array means exactly those lists. Every ID must belong to this company."
      responses:
        "200":
          description: Ingestion controls updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  integrationId:
                    type: string
                  provider:
                    type: string
                  syncEnabled:
                    type: boolean
                    description: Bulk import and backfill state after the update.
                  listTargeting:
                    type: string
                    nullable: true
                    enum:
                      - company_default
                      - none
                      - specific
                    description: Where contacts created by this integration land. Null for providers that ignore per-integration list targeting.
                  listIds:
                    type: array
                    nullable: true
                    items:
                      type: string
                    description: Configured target lists. Null means new contacts follow the workspace default lists.
                  changed:
                    type: boolean
                    description: False when the integration was already in the requested state.
                  changedFields:
                    type: array
                    items:
                      type: string
                    description: Which controls actually moved.
                  message:
                    type: string
        "400":
          description: Integration is disconnected, no field was provided, the provider does not expose the requested control, or a list ID is invalid or belongs to another company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: A sync is already in progress; wait for it to finish before disabling
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /integrations/{id}/sync:
    post:
      operationId: syncIntegration
      summary: Sync integration
      description: Queues a manual re-sync for a connected integration - customers and revenue for a payment provider (Stripe, Polar, Paddle, Dodo, Creem, Chargebee, Whop), the user backfill for Supabase, or the event-history import for PostHog and Segment. The Supabase sync reads the project, schema, and table already configured for the integration and returns 400 when none is configured. PostHog and Segment re-run their event-history imports with credentials stored at connect time and are the supported retry path for failed imports; each restarts from the beginning, already-imported events dedupe, and returns 409 while queued or syncing. Segment requires a saved Unify space ID and Profile API token and covers the most recent 14 days served by the Profile API. Terminal BullMQ failures release imports for retry. Returns immediately; poll the integration to watch syncStatus. Other providers re-sync from the dashboard. Requires the integrations:manage scope.
      tags:
        - Integrations
      parameters:
        - name: id
          in: path
          required: true
          description: Integration ID.
          schema:
            type: string
      responses:
        "200":
          description: Sync queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  integrationId:
                    type: string
                  provider:
                    type: string
                  jobId:
                    type: string
                    nullable: true
                  syncStatus:
                    type: string
                    example: queued
                  syncTarget:
                    type: object
                    nullable: true
                    description: Supabase only - the source the backfill reads. Absent for providers whose sync has no configurable source.
                    properties:
                      projectRef:
                        type: string
                      schema:
                        type: string
                      table:
                        type: string
                  message:
                    type: string
        "400":
          description: Provider does not support an API-triggered sync, the Supabase sync has no configured table, or the integration is disconnected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: A sync is already in progress or bulk sync is disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /integrations/{id}/pixel:
    get:
      operationId: getIntegrationPixel
      summary: Get storefront pixel state
      description: Reads the live state of a Shopify store's storefront tracking pixel. Nothing about the pixel is stored locally, so this queries the store on every call. A confirmed missing or stale pixel prevents on-site events (product views, cart activity, browse abandonment) from arriving; a Shopify read error reports the state as unknown instead. Shopify only. Requires the account:read scope.
      tags:
        - Integrations
      parameters:
        - name: id
          in: path
          required: true
          description: Shopify integration ID.
          schema:
            type: string
      responses:
        "200":
          description: Pixel state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntegrationPixelState"
        "400":
          description: Provider has no storefront pixel, or the integration is disconnected or missing credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: activateIntegrationPixel
      summary: Activate storefront pixel
      description: Installs the Shopify storefront tracking pixel, or repoints an existing one at this account. Idempotent - an already-live pixel returns changed false without writing to the store. Events start arriving on the next storefront visit; nothing is backfilled. Fails with a 400 naming the reconnect step when the store granted an older permission set. Shopify only. Requires the integrations:manage scope.
      tags:
        - Integrations
      parameters:
        - name: id
          in: path
          required: true
          description: Shopify integration ID.
          schema:
            type: string
      responses:
        "200":
          description: Pixel installed, repointed, or already live
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/IntegrationPixelState"
                  - type: object
                    properties:
                      changed:
                        type: boolean
                        description: False when the pixel was already installed and current.
                      created:
                        type: boolean
                        description: True when a new pixel was installed.
                      updated:
                        type: boolean
                        description: True when an existing pixel was repointed.
        "400":
          description: Provider has no storefront pixel, the integration is disconnected, or Shopify refused the install
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sender-profiles:
    get:
      operationId: listSenderProfiles
      summary: List sender and reply-to profiles
      description: Lists sender (From) and reply-to profiles, which are the account defaults, and whether each sender address sits on a verified sending domain. SMTP submission sends into Sequenzy; outbound delivery remains Sequenzy-managed through SES or Sequenzy's MTA, so customer-managed SMTP relays are not supported.
      tags:
        - Sender Profiles
      responses:
        "200":
          description: Sending identities returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  senderProfiles:
                    type: array
                    items:
                      $ref: "#/components/schemas/SenderProfileSummary"
                  replyProfiles:
                    type: array
                    items:
                      $ref: "#/components/schemas/ReplyProfileSummary"
                  defaultSenderProfileId:
                    type: string
                    nullable: true
                  defaultReplyProfileId:
                    type: string
                    nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sender-profiles/{id}:
    patch:
      operationId: updateSenderProfile
      summary: Rename a sender profile
      description: Renames one sender (From) profile in place. Only the display name changes - the address, its sending domain, and the account-wide default From selection are left untouched, so a display name can be standardized across the several identities one mailbox may carry. To change which profile is the account default instead, use PATCH /v1/companies/{companyId} with senderProfileId. Requires companies:manage.
      tags:
        - Sender Profiles
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Sender profile ID, from GET /v1/sender-profiles.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: New display name. Trimmed before saving.
                  example: SnapCount
      responses:
        "200":
          description: Sender profile renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  senderProfile:
                    $ref: "#/components/schemas/SenderProfileSummary"
                  renamed:
                    type: boolean
                    description: False when the profile already carried that name, so nothing changed.
        "400":
          description: Name is empty or longer than 255 characters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sender profile not found in this company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Another sender identity for the same address already uses that name
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /reply-profiles/{id}:
    patch:
      operationId: updateReplyProfile
      summary: Rename a reply-to profile
      description: Renames one reply-to profile in place. Only the display name changes - the address and the account-wide default Reply-To selection are left untouched. Requires companies:manage.
      tags:
        - Sender Profiles
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Reply-to profile ID, from GET /v1/sender-profiles.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: New display name. Trimmed before saving.
                  example: SnapCount
      responses:
        "200":
          description: Reply-to profile renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  replyProfile:
                    $ref: "#/components/schemas/ReplyProfileSummary"
                  renamed:
                    type: boolean
                    description: False when the profile already carried that name, so nothing changed.
        "400":
          description: Name is empty or longer than 255 characters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Reply-to profile not found in this company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /notification-preferences:
    get:
      operationId: getNotificationPreferences
      summary: Get account notification preferences
      description: Returns the account notification settings for the API key's own user in the active company, along with the modes each event supports and the platform defaults. Every event is always present; an event the user has never configured reports its default. There is no way to read another member's preferences through this API. Requires account:read.
      tags:
        - Notification Preferences
      responses:
        "200":
          description: Notification preferences returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationPreferences"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      operationId: updateNotificationPreferences
      summary: Update account notification preferences
      description: Changes which account notifications Sequenzy emails the API key's own user for the active company. Events not listed keep their current value. Useful before a bulk import or migration, though imports never trigger new-subscriber notifications in the first place. Requires companies:manage; account:read alone cannot mutate these settings.
      tags:
        - Notification Preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - notificationPreferences
              properties:
                notificationPreferences:
                  type: array
                  minItems: 1
                  description: Preferences to set. Events not listed are left unchanged.
                  items:
                    $ref: "#/components/schemas/NotificationPreference"
      responses:
        "200":
          description: Notification preferences updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationPreferences"
        "400":
          description: Unsupported event or mode. "daily" is not valid for campaign_completed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /tracking-settings:
    get:
      operationId: getTrackingSettings
      summary: Get tracking settings
      description: Returns open, click, and unsubscribe tracking flags, the default attribution window, automatic UTM tagging, the dedicated click-tracking domain, inbound reply tracking settings, and whether double opt-in is required for new contacts.
      tags:
        - Tracking Settings
      responses:
        "200":
          description: Tracking configuration returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackingSettings"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      operationId: updateTrackingSettings
      summary: Update tracking settings
      description: Updates the account-wide tracking defaults - open, click, and unsubscribe tracking, strict bot filtering, the default attribution window, and automatic UTM tagging - plus the double opt-in requirement for new contacts. Applies to emails sent afterwards; already-sent emails keep the links they were rendered with. Reply tracking is updated through the company endpoint.
      tags:
        - Tracking Settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                openTrackingEnabled:
                  type: boolean
                  description: Whether to embed the open-tracking pixel.
                clickTrackingEnabled:
                  type: boolean
                  description: Whether to rewrite links through the click-tracking redirect.
                strictBotFilteringEnabled:
                  type: boolean
                  description: Opt-in aggressive bot detection (strict user-agent patterns, datacenter IPs, cross-send IP sweeps). Off by default; enabling it can lower reported open and click rates.
                unsubscribeTrackingEnabled:
                  type: boolean
                  description: Whether unsubscribe links are attributed to the email that produced them.
                defaultAttributionWindowHours:
                  type: integer
                  minimum: 1
                  maximum: 720
                  description: Default revenue attribution window in hours.
                doubleOptInEnabled:
                  type: boolean
                  description: Whether new contacts must confirm by email before they become subscribed. This is the account-wide default that the per-request optInMode on subscriber creation overrides. Enabling it requires a sender profile and provisions the confirmation email automatically; it does not change contacts that are already active.
                autoUtmEnabled:
                  type: boolean
                  description: Whether UTM parameters are appended to outbound links automatically. Enabling this with no stored parameters seeds the platform defaults.
                autoUtmSettings:
                  type: object
                  nullable: true
                  description: UTM templates merged over the stored ones. Null resets every parameter to the platform defaults; a null field stops that parameter being emitted.
                  properties:
                    source:
                      type: string
                      nullable: true
                    medium:
                      type: string
                      nullable: true
                    campaign:
                      type: string
                      nullable: true
                    content:
                      type: string
                      nullable: true
                    term:
                      type: string
                      nullable: true
      responses:
        "200":
          description: Tracking configuration updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/TrackingSettings"
                  - type: object
                    properties:
                      message:
                        type: string
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sending-status:
    get:
      operationId: getSendingStatus
      summary: Get sending status
      description: Returns whether company-level sending is active, paused, or suspended, the pause reason, the sender-health counts and thresholds behind it, the automated review state, whether sending can be restored without support, and ordered remediation steps. Call this whenever a send or test send fails for a reason that is not a validation error. Enforcement uses all-time totals from a reset watermark rather than a rolling window, so metricsWindow.expiresAt is always null and waiting does not restore sending. Requires the account:read scope.
      tags:
        - Sending Status
      responses:
        "200":
          description: Sending status returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendingStatus"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the account:read scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sending-status/resume:
    post:
      operationId: resumeSending
      summary: Resume paused sending
      description: Restores company-level sending paused by a high permanent-bounce rate, after the cause has been fixed. This is not a bypass - it enforces the same gates as the dashboard and never removes suppressions. For a paused workspace, sending is restored only when selfResume.canSelfResume is true on GET /sending-status, which requires a high_hard_bounce_rate pause, a cleared automated sender-health review, and no admin block. An already-active workspace succeeds as an idempotent no-op with resumed false. On restoration the bounce watermark moves to now and the service attempts to requeue paused campaigns plus due sequence steps. A partial queue handoff still returns the committed active state with recovery guidance in message. Requires the companies:manage scope plus owner or admin access.
      tags:
        - Sending Status
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - listSanitizationConfirmed
              properties:
                listSanitizationConfirmed:
                  type: boolean
                  description: Must be true. Confirms the source of the invalid addresses is fixed and permanent bounces remain suppressed. Recorded on the account audit trail.
      responses:
        "200":
          description: Sending resumed, or already active
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SendingStatus"
                  - type: object
                    properties:
                      resumed:
                        type: boolean
                        description: False when sending was already active.
                      message:
                        type: string
                        description: Confirms the outcome, or explains that sending resumed while some parked work still needs support.
        "400":
          description: Confirmation missing, or the pause reason is not self-resumable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Blocked by the automated review, an admin block, or view-only access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Website Operations
  # ============================================================================
  /websites:
    get:
      operationId: listWebsites
      summary: List websites
      description: Lists sending domains configured for the authenticated company.
      tags:
        - Websites
      responses:
        "200":
          description: Websites returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  websites:
                    type: array
                    items:
                      $ref: "#/components/schemas/Website"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: addWebsite
      summary: Add sending domain
      description: Adds a sending domain to the authenticated company and returns the SPF, DKIM, MAIL FROM, and inbound DNS records required for setup.
      tags:
        - Websites
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                domain:
                  type: string
                  description: Domain to add.
              required:
                - domain
      responses:
        "200":
          description: Website added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  website:
                    $ref: "#/components/schemas/Website"
        "400":
          description: Validation or provisioning error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Provisioning forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Provisioning resource not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Domain conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentFriendlyError"
        "500":
          description: Website could not be added
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /websites/{domain}:
    get:
      operationId: getWebsite
      summary: Get website
      description: Returns verification status and DNS records for a sending domain.
      tags:
        - Websites
      parameters:
        - name: domain
          in: path
          required: true
          schema:
            type: string
          description: Sending domain
      responses:
        "200":
          description: Website returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  website:
                    $ref: "#/components/schemas/Website"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentFriendlyError"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /websites/{domain}/verify:
    post:
      operationId: verifySendingDomain
      summary: Verify sending domain
      description: Runs a fresh DNS and provider verification and returns normalized aggregate, SPF, DKIM, and MAIL FROM status and diagnostics.
      tags:
        - Websites
      parameters:
        - name: domain
          in: path
          required: true
          schema:
            type: string
          description: Configured sending domain
      responses:
        "200":
          description: Verification completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  website:
                    $ref: "#/components/schemas/Website"
                  verified:
                    type: boolean
                    description: Fresh DNS verification verdict. Correct DNS alone does not mean the domain can send yet.
                  readyToSend:
                    type: boolean
                    description: Whether the domain is fully ready to send (DNS verified and activation finished).
                  message:
                    type: string
                  discarded:
                    type: boolean
        "400":
          description: Verification cannot start because setup is incomplete
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Domain not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentFriendlyError"

  /products/{productId}/delivery:
    put:
      operationId: attachProductDelivery
      summary: Attach delivery file to product
      description: Attaches the distributable file delivered after a purchase of this product. Purchase events then expose it as download.url / download.name. Accepts the internal product id or, for Commerce API products, your productId.
      tags:
        - Products
      parameters:
        - name: productId
          in: path
          required: true
          description: Internal product id, or your own productId for products pushed via the Commerce API.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: Public http(s) URL of the file.
                source:
                  type: string
                  enum: [upload, url]
                  default: url
                fileName:
                  type: string
                fileSizeBytes:
                  type: integer
                mimeType:
                  type: string
      responses:
        "200":
          description: Delivery attached
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  product:
                    $ref: "#/components/schemas/Product"
        "400":
          description: Invalid URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      operationId: removeProductDelivery
      summary: Remove delivery file from product
      description: Removes the attached distributable file from a product. Accepts the internal product id or, for Commerce API products, your productId.
      tags:
        - Products
      parameters:
        - name: productId
          in: path
          required: true
          description: Internal product id, or your own productId for products pushed via the Commerce API.
          schema:
            type: string
      responses:
        "200":
          description: Delivery removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  product:
                    $ref: "#/components/schemas/Product"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /products/delivery/upload-url:
    post:
      operationId: createProductDeliveryUploadUrl
      summary: Create delivery file upload URL
      description: Returns a presigned URL to upload a distributable file. PUT the file bytes to uploadUrl, then attach publicUrl to a product.
      tags:
        - Products
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, contentType, fileSizeBytes]
              properties:
                filename:
                  type: string
                contentType:
                  type: string
                  example: "application/pdf"
                fileSizeBytes:
                  type: integer
                  maximum: 104857600
      responses:
        "200":
          description: Upload URL created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  uploadUrl:
                    type: string
                  publicUrl:
                    type: string
                  fileName:
                    type: string
        "400":
          description: Unsupported file type or size
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Uploads not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /products/sync:
    post:
      operationId: syncStripeProducts
      summary: Sync Stripe products
      description: Queues a sync of the Stripe product catalog into the products list. Requires an active Stripe integration with bulk sync enabled.
      tags:
        - Products
      parameters:
        - name: integrationId
          in: query
          required: false
          description: Stripe integration to sync. When omitted, the most recently connected active integration with bulk sync enabled is used.
          schema:
            type: string
      responses:
        "200":
          description: Sync queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  jobId:
                    type: string
                    nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No active Stripe integration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Every active Stripe integration has bulk sync disabled, or the selected integration has bulk sync disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /media/upload-url:
    post:
      operationId: createEmailImageUploadUrl
      summary: Create email image upload URL
      description: Returns an authenticated API URL for a block-ready email image. PUT the exact bytes to uploadUrl using the same API credentials, then register the key with POST /media/complete-upload. The public object does not exist until its bytes pass server-side validation.
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, contentType, fileSizeBytes]
              properties:
                filename:
                  type: string
                contentType:
                  type: string
                  example: "image/png"
                fileSizeBytes:
                  type: integer
                  minimum: 1
                  maximum: 5242880
      responses:
        "200":
          description: Upload URL created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  uploadUrl:
                    type: string
                  publicUrl:
                    type: string
                  key:
                    type: string
                  fileName:
                    type: string
        "400":
          description: Unsupported image type or size
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Uploads not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /media/upload-bytes:
    put:
      operationId: uploadEmailImageBytes
      summary: Upload validated email image bytes
      description: Uploads the exact bytes to the authenticated URL returned by POST /media/upload-url. The server enforces the requested size, verifies the file signature, and creates the public object only once.
      tags:
        - Media
      parameters:
        - in: query
          name: key
          required: true
          schema:
            type: string
        - in: query
          name: filename
          required: true
          schema:
            type: string
        - in: query
          name: contentType
          required: true
          schema:
            type: string
        - in: query
          name: fileSizeBytes
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 5242880
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Image bytes validated and stored
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  key:
                    type: string
                  publicUrl:
                    type: string
        "400":
          description: Invalid size, key, content type, or image bytes
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Upload URL already used
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Uploads are not configured, or the upload service or database is temporarily unavailable. Retry-After is present for a transient database failure.
          headers:
            Retry-After:
              $ref: "#/components/headers/RetryAfter"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /media/complete-upload:
    post:
      operationId: completeEmailImageUpload
      summary: Register uploaded email image
      description: Idempotently registers a completed company-scoped image upload in the shared media library and returns its hosted URL.
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key, filename, contentType, fileSizeBytes, altText]
              properties:
                key:
                  type: string
                filename:
                  type: string
                contentType:
                  type: string
                  example: "image/png"
                fileSizeBytes:
                  type: integer
                  minimum: 1
                  maximum: 5242880
                width:
                  type: integer
                  minimum: 1
                  maximum: 20000
                height:
                  type: integer
                  minimum: 1
                  maximum: 20000
                altText:
                  type: string
                  maxLength: 500
      responses:
        "200":
          description: Image registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  asset:
                    type: object
                    properties:
                      id:
                        type: string
                      filename:
                        type: string
                      url:
                        type: string
                      mimeType:
                        type: string
                      size:
                        type: string
                      width:
                        type: string
                        nullable: true
                      height:
                        type: string
                        nullable: true
                      altText:
                        type: string
                        nullable: true
                      companyId:
                        type: string
                      createdAt:
                        type: string
                        format: date-time
        "400":
          description: Invalid or cross-company upload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Image changed while completion was verifying it
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Uploads not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /tags:
    get:
      operationId: listTags
      summary: List tags
      description: Lists tag definitions for the authenticated company.
      tags:
        - Tags
      responses:
        "200":
          description: Tags returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  tags:
                    type: array
                    items:
                      $ref: "#/components/schemas/TagDefinition"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createTag
      summary: Create tag
      description: Creates a tag definition. Tag names are normalized to lowercase with spaces replaced by hyphens.
      tags:
        - Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  minLength: 1
                  description: Tag name. Normalized to lowercase with spaces replaced by hyphens.
                  example: "premium"
                color:
                  type: string
                  description: "Tag color. One of: gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose. Defaults to gray."
                  example: "blue"
              required:
                - name
      responses:
        "200":
          description: Tag created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  tag:
                    $ref: "#/components/schemas/TagDefinition"
        "400":
          description: Invalid tag color
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: A tag with this name already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Tag could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /tags/{tagId}:
    patch:
      operationId: updateTag
      summary: Update tag
      description: Updates a tag definition's color. System tags cannot be updated.
      tags:
        - Tags
      parameters:
        - name: tagId
          in: path
          required: true
          schema:
            type: string
          description: Tag definition ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                color:
                  type: string
                  description: "Tag color. One of: gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose."
                  example: "green"
              required:
                - color
      responses:
        "200":
          description: Tag updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  tag:
                    $ref: "#/components/schemas/TagDefinition"
        "400":
          description: Invalid tag color
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Cannot update system tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Tag not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteTag
      summary: Delete tag
      description: Deletes a tag definition and removes the tag from all subscribers. Fails when the tag is referenced by sequences or is a system tag.
      tags:
        - Tags
      parameters:
        - name: tagId
          in: path
          required: true
          schema:
            type: string
          description: Tag definition ID.
      responses:
        "200":
          description: Tag deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Cannot delete system tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Tag not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Tag is used in one or more sequences
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /lists:
    get:
      operationId: listSubscriberLists
      summary: List subscriber lists
      description: Lists subscriber lists for the authenticated company. Each list includes subscriberCount (current members of any status) and activeSubscriberCount (current members with status=active). Members who unsubscribed from the list are not counted.
      tags:
        - Lists
      responses:
        "200":
          description: Lists returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  lists:
                    type: array
                    items:
                      $ref: "#/components/schemas/SubscriberList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createSubscriberList
      summary: Create subscriber list
      description: Creates a subscriber list for grouping contacts.
      tags:
        - Lists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                description:
                  type: string
                isPrivate:
                  type: boolean
                  description: Set to true to keep the list internal and omit it from individual controls on the hosted subscriber email preferences/unsubscribe page. Public lists expose their name and description on that page. List privacy does not override a subscriber's global unsubscribe. Defaults to false when omitted.
              required:
                - name
      responses:
        "200":
          description: List created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  list:
                    $ref: "#/components/schemas/SubscriberList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: List could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /lists/{listId}/subscribers:
    post:
      operationId: addSubscribersToList
      summary: Add subscribers to list
      description: Adds existing or new subscribers to one subscriber list from an email array. Use this endpoint without a `/bulk` suffix. Requires the lists:write and subscribers:write scopes.
      tags:
        - Lists
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
          description: Subscriber list ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                emails:
                  type: array
                  maxItems: 500
                  items:
                    type: string
                    format: email
                  description: Up to 500 email addresses per request.
                duplicateStrategy:
                  type: string
                  enum:
                    - skip
                    - merge
                    - overwrite
                  default: skip
                enrollInSequences:
                  type: boolean
                  default: false
                optInMode:
                  type: string
                  enum:
                    - default
                    - confirmed
                    - double_opt_in
                  default: default
              required:
                - emails
      responses:
        "200":
          description: Subscribers processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  listId:
                    type: string
                  total:
                    type: integer
                  processed:
                    type: integer
                  created:
                    type: integer
                  updated:
                    type: integer
                  skipped:
                    type: integer
                  addedToList:
                    type: integer
                  failed:
                    type: integer
                  duplicateInputCount:
                    type: integer
                  ignoredBlankCount:
                    type: integer
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        success:
                          type: boolean
                        created:
                          type: boolean
                        updated:
                          type: boolean
                        skipped:
                          type: boolean
                        addedToList:
                          type: boolean
                        error:
                          type: string
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: List not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Subscribers could not be added
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /lists/{listId}:
    patch:
      operationId: updateSubscriberList
      summary: Update subscriber list
      description: Updates a subscriber list's name, description, or privacy flag. Only provided fields are changed.
      tags:
        - Lists
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
          description: Subscriber list ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  minLength: 1
                  description: New list name.
                description:
                  type: string
                  nullable: true
                  description: New list description. Pass null to clear it.
                isPrivate:
                  type: boolean
                  description: Set to true to keep the list internal and omit it from individual controls on the hosted subscriber email preferences/unsubscribe page. Set to false to expose its name and description on that page. List privacy does not override a subscriber's global unsubscribe. Omit this field to leave the current visibility unchanged.
      responses:
        "200":
          description: List updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  list:
                    $ref: "#/components/schemas/SubscriberList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: List not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: List could not be updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteSubscriberList
      summary: Delete subscriber list
      description: Deletes a subscriber list and removes all list memberships. Subscribers themselves are not deleted.
      tags:
        - Lists
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
          description: Subscriber list ID.
      responses:
        "200":
          description: List deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  removedMemberships:
                    type: integer
                    description: Number of list memberships removed.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: List not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /lists/{listId}/subscribers/remove:
    post:
      operationId: removeSubscribersFromList
      summary: Remove subscribers from list
      description: Removes subscribers from one subscriber list by email or subscriber ID. Subscribers themselves are not deleted. Requires the lists:write and subscribers:write scopes, the same as adding them.
      tags:
        - Lists
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
          description: Subscriber list ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                emails:
                  type: array
                  items:
                    type: string
                    format: email
                  description: Email addresses to remove. Combined with subscriberIds, up to 500 per request.
                subscriberIds:
                  type: array
                  items:
                    type: string
                  description: Subscriber IDs to remove. Combined with emails, up to 500 per request.
      responses:
        "200":
          description: Subscribers removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  removed:
                    type: integer
                    description: Number of list memberships removed.
                  notFound:
                    type: array
                    items:
                      type: string
                    description: Emails that did not match any subscriber.
        "400":
          description: No targets provided or too many targets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: List not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Campaign Operations
  # ============================================================================
  /campaigns:
    get:
      operationId: listCampaigns
      summary: List campaigns
      description: Lists campaigns for the authenticated company, optionally filtered by status or label.
      tags:
        - Campaigns
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              [
                draft,
                scheduled,
                sent,
                sending,
                cancelled,
                paused,
                waiting_approval,
                rejected,
              ]
          description: Optional campaign status filter.
        - name: label
          in: query
          required: false
          schema:
            type: string
          description: Optional label name filter. Only campaigns assigned this label are returned.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Optional page size. Values above 100 are capped to 100.
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Optional zero-based row offset.
      responses:
        "200":
          description: Campaigns returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaigns:
                    type: array
                    items:
                      $ref: "#/components/schemas/CampaignSummary"
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                        example: 50
                      offset:
                        type: integer
                        example: 0
                      count:
                        type: integer
                        example: 50
                      total:
                        type: integer
                        example: 125
                      hasMore:
                        type: boolean
                        example: true
        "400":
          description: Invalid filter or pagination parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createCampaign
      summary: Create campaign
      description: Creates a campaign and linked email from at most one of prompt, HTML, Sequenzy blocks, or an existing template. Omit all content sources to create an empty draft. Optional From/Reply-To inputs create or select profiles; From addresses require a verified sending domain. Defaults to draft. Use status `sent` only to archive an imported/already-sent campaign.
      tags:
        - Campaigns
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                subject:
                  type: string
                  description: Required with HTML, blocks, or templateId; optional with prompt, where it overrides the generated subject.
                previewText:
                  type: string
                  nullable: true
                  description: Optional inbox preview text saved on the linked email.
                preheaderText:
                  type: string
                  nullable: true
                  description: Compatibility alias for previewText.
                trackingCode:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Optional campaign tracking code available to UTM templates as `{{campaign.trackingCode}}`. Empty strings are stored as null.
                status:
                  type: string
                  enum: [draft, sent]
                  description: Initial status. Defaults to draft. Use sent only for imported/already-sent campaigns.
                sentAt:
                  type: string
                  nullable: true
                  format: date-time
                  description: ISO date-time for an imported/already-sent campaign. Only valid with status sent; defaults to now when omitted.
                html:
                  type: string
                  description: Raw HTML body. Mutually exclusive with blocks.
                blocks:
                  type: array
                  description: Sequenzy email blocks. Mutually exclusive with html. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
                  items:
                    type: object
                    additionalProperties: true
                prompt:
                  type: string
                  minLength: 1
                  description: Natural-language request for branded native campaign blocks.
                style:
                  type: string
                  description: Generation style; valid only with prompt.
                tone:
                  type: string
                  description: Generation tone; valid only with prompt.
                campaignData:
                  type: object
                  nullable: true
                  additionalProperties: true
                computedLists:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
                labels:
                  type: array
                  description: Label names to assign. Missing labels are created automatically.
                  items:
                    type: string
                label:
                  type: array
                  description: Compatibility alias for labels.
                  items:
                    type: string
                templateId:
                  type: string
                  minLength: 1
                  description: Company-owned email template to copy into the campaign. Mutually exclusive with prompt, HTML, and blocks.
                segmentId:
                  type: string
                  description: Shorthand for targeting one saved segment. Equivalent to `targetLists` `{"type":"segment","segmentId":"seg_123"}`. Mutually exclusive with targetLists and listIds.
                listIds:
                  type: array
                  items:
                    type: string
                  description: Shorthand for targeting one or more lists. Equivalent to `targetLists` `{"type":"lists","listIds":["list_123"]}`. Mutually exclusive with targetLists and segmentId.
                targetLists:
                  type: object
                  additionalProperties: true
                  description: 'Campaign audience saved on the draft. Omit to leave targeting unset and choose it when scheduling. The object is a union discriminated on type: {"type":"all"}, {"type":"lists","listIds":["list_123"]}, {"type":"segment","segmentId":"seg_123"}, {"type":"filtered","filters":[],"filterJoinOperator":"and"}, {"type":"rules","include":[],"exclude":[]}. Mutually exclusive with segmentId and listIds.'
                fromEmail:
                  type: string
                  format: email
                  description: Campaign From address. Its domain must be configured and verified.
                fromName:
                  type: string
                  description: Display name recipients see, e.g. 'Brennon at TradeTally'. Selects the sender identity of that name on fromEmail, creating it when the address has no identity by that name; the mailbox's other display names, and everything pinned to them, are untouched. Requires fromEmail; omit it when using senderProfileId, which already carries its own display name.
                senderProfileId:
                  type: string
                  description: Existing sender profile ID. It already supplies both the From address and display name, so send it on its own and omit fromEmail and fromName.
                replyTo:
                  type: string
                  format: email
                  description: Campaign Reply-To address. A reply profile is created when needed.
                replyToName:
                  type: string
                  description: Display name for the Reply-To address. Requires replyTo; omit it when using replyProfileId, which already carries its own display name. An address carries one Reply-To name company-wide, so if replyTo already has a saved profile under a different name, that saved name is kept and the response `warnings` array says so.
                replyProfileId:
                  type: string
                  description: Existing reply profile ID. It already supplies both the Reply-To address and display name, so send it on its own and omit replyTo and replyToName.
              required:
                - name
            examples:
              create-from-html:
                summary: Create from HTML
                value:
                  name: "April Launch"
                  subject: "A quick update"
                  preheaderText: "A short preview for the inbox"
                  html: "<p>Hello there!</p>"
                  labels: ["edm", "api"]
              create-imported-sent:
                summary: Create imported sent campaign
                value:
                  name: "Imported April Launch"
                  subject: "A quick update"
                  status: sent
                  sentAt: "2026-05-01T14:00:00Z"
                  html: "<p>Hello there!</p>"
              create-from-blocks:
                summary: Create from blocks
                value:
                  name: "April Launch"
                  subject: "A quick update"
                  blocks:
                    - type: text
                      content: "<p>Hello there!</p>"
      responses:
        "200":
          description: Campaign created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      subject:
                        type: string
                      emailId:
                        type: string
                        description: The linked email body, reusable as `templateId` when creating later campaigns.
                      status:
                        type: string
                        example: "draft"
                      sentAt:
                        type: string
                        format: date-time
                        nullable: true
                        description: Present when the campaign is created as an imported/already-sent campaign.
                      trackingCode:
                        type: string
                        nullable: true
                        description: Campaign tracking code available to UTM templates as `{{campaign.trackingCode}}`.
                      labels:
                        type: array
                        items:
                          type: string
                        example: ["edm", "api"]
                      targetLists:
                        type: object
                        nullable: true
                        additionalProperties: true
                        description: Saved campaign audience, or null when targeting is still unset.
                      senderProfileId:
                        type: string
                      fromName:
                        type: string
                      fromEmail:
                        type: string
                        format: email
                      replyProfileId:
                        type: string
                        nullable: true
                      replyToName:
                        type: string
                        nullable: true
                      replyToEmail:
                        type: string
                        format: email
                        nullable: true
                      url:
                        type: string
                        format: uri
                        example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123"
                      previewUrl:
                        type: string
                        format: uri
                        example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error or missing sender profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Campaign could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}:
    get:
      operationId: getCampaign
      summary: Get campaign
      description: >-
        Returns one campaign with its email blocks, campaign data, reply-to
        profile, and schedule timestamps. Poll this to follow a campaign held in
        waiting_approval: on approval the status returns to scheduled (or
        sending, if the scheduled time already passed), and on rejection it
        becomes rejected with reviewer feedback in rejectionComment.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Campaign returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    $ref: "#/components/schemas/CampaignDetail"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateCampaign
      summary: Update a draft campaign
      description: Update a draft campaign's name, labels, content, audience, From/Reply-To settings, or campaign personalization data. Direct addresses create profiles when needed.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  description: Updated campaign name
                  example: "April Launch"
                subject:
                  type: string
                  description: Updated email subject line
                  example: "A quick update"
                previewText:
                  type: string
                  nullable: true
                  description: Updated inbox preview text. Set to null to clear it.
                  example: "A short preview for the inbox"
                preheaderText:
                  type: string
                  nullable: true
                  description: Compatibility alias for previewText.
                  example: "A short preview for the inbox"
                trackingCode:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: Campaign tracking code available to UTM templates as `{{campaign.trackingCode}}`. Send an empty string or null to clear it.
                html:
                  type: string
                  description: Updated email HTML content. Mutually exclusive with `blocks`.
                  example: "<p>Hello there!</p>"
                blocks:
                  type: array
                  description: Updated Sequenzy email blocks. Mutually exclusive with `html`. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
                  items:
                    type: object
                fromEmail:
                  type: string
                  format: email
                  description: Campaign From address. Its domain must be configured and verified.
                fromName:
                  type: string
                  description: Display name recipients see, e.g. 'Brennon at TradeTally'. Selects the sender identity of that name on fromEmail, creating it when the address has no identity by that name; the mailbox's other display names, and everything pinned to them, are untouched. Requires fromEmail; omit it when using senderProfileId, which already carries its own display name.
                senderProfileId:
                  type: string
                  description: Existing sender profile ID. It already supplies both the From address and display name, so send it on its own and omit fromEmail and fromName.
                replyTo:
                  type: string
                  format: email
                  description: Reply-To email for this campaign. A profile is created when needed. Mutually exclusive with `replyProfileId`.
                  example: "support@example.com"
                replyProfileId:
                  type: string
                  description: Reply profile ID for this company. It already supplies both the Reply-To address and display name, so send it on its own and omit replyTo and replyToName.
                  example: "reply_abc123"
                replyToName:
                  type: string
                  description: Display name for the Reply-To address. Requires replyTo; omit it when using replyProfileId, which already carries its own display name. An address carries one Reply-To name company-wide, so if replyTo already has a saved profile under a different name, that saved name is kept and the response `warnings` array says so.
                ccEmails:
                  type: array
                  nullable: true
                  maxItems: 10
                  description: Addresses CC'd on every recipient's email for this campaign. Send an empty array or null to clear them.
                  items:
                    type: string
                    format: email
                  example: ["ops@example.com"]
                bccEmails:
                  type: array
                  nullable: true
                  maxItems: 10
                  description: Addresses BCC'd on every recipient's email for this campaign. Send an empty array or null to clear them.
                  items:
                    type: string
                    format: email
                  example: ["archive@example.com"]
                labels:
                  type: array
                  description: Replacement label names. Send an empty array to clear labels. Missing labels are created automatically.
                  items:
                    type: string
                  example: ["edm", "api"]
                label:
                  type: array
                  description: Compatibility alias for labels.
                  items:
                    type: string
                  example: ["edm", "api"]
                campaignData:
                  type: object
                  nullable: true
                  description: Campaign-scoped JSON data available while rendering this campaign. Top-level arrays can contain up to 500 items. Set to null to clear it.
                  additionalProperties: true
                  example:
                    events:
                      - id: "evt_123"
                        title: "Romeo and Juliet"
                        region: "Auckland"
                        genre: "theatre"
                        url: "https://example.com/events/evt_123"
                computedLists:
                  type: array
                  description: Personalized list definitions computed from campaignData. Keys can use letters, numbers, underscores, and dots. Use maxItems to cap each subscriber's list length. Pass an empty array to clear computed lists.
                  maxItems: 20
                  items:
                    type: object
                    additionalProperties: true
                  example:
                    - key: "recommendedEvents"
                      source: "events"
                      maxItems: 6
                      rules:
                        - itemField: "region"
                          operator: "equals"
                          subscriberField: "region"
                        - itemField: "genre"
                          operator: "in"
                          subscriberField: "interests"
                targetLists:
                  type: object
                  nullable: true
                  additionalProperties: true
                  description: 'Replacement campaign audience, using the same shapes as campaign create, e.g. {"type":"lists","listIds":["list_123"]}. Send null to clear saved targeting and choose the audience when scheduling; omit to leave it unchanged. Mutually exclusive with segmentId and listIds.'
                segmentId:
                  type: string
                  description: Shorthand for retargeting the draft at one saved segment. Equivalent to `targetLists` `{"type":"segment","segmentId":"seg_123"}`. Mutually exclusive with targetLists and listIds.
                listIds:
                  type: array
                  items:
                    type: string
                  description: Shorthand for retargeting the draft at one or more lists. Equivalent to `targetLists` `{"type":"lists","listIds":["list_123"]}`. Mutually exclusive with targetLists and segmentId.
            examples:
              update-subject:
                summary: Update campaign subject
                value:
                  subject: "A quick update"
              update-target-lists:
                summary: Retarget the draft at a list
                value:
                  targetLists:
                    type: "lists"
                    listIds: ["list_123"]
              clear-target-lists:
                summary: Clear saved targeting
                value:
                  targetLists: null
              update-preheader:
                summary: Update campaign preheader text
                value:
                  preheaderText: "A short preview for the inbox"
              update-reply-to:
                summary: Update campaign reply-to
                value:
                  replyTo: "support@example.com"
              update-labels:
                summary: Replace campaign labels
                value:
                  labels: ["edm", "api"]
              update-blocks:
                summary: Update campaign blocks
                value:
                  blocks:
                    - id: pro-offer
                      type: text
                      content: "<p>Your Pro upgrade is ready.</p>"
                      variant: paragraph
                      condition:
                        variable: plan
                        operator: equals
                        value: pro
              update-campaign-data:
                summary: Update campaign data and computed list rules
                value:
                  campaignData:
                    events:
                      - id: "evt_123"
                        title: "Romeo and Juliet"
                        region: "Auckland"
                        genre: "theatre"
                  computedLists:
                    - key: "recommendedEvents"
                      source: "events"
                      maxItems: 6
                      rules:
                        - itemField: "region"
                          operator: "equals"
                          subscriberField: "region"
      responses:
        "200":
          description: Campaign updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "camp_abc123"
                      name:
                        type: string
                        example: "April Launch"
                      subject:
                        type: string
                        example: "A quick update"
                      emailId:
                        type: string
                        description: The linked email body, reusable as `templateId` when creating later campaigns.
                      status:
                        type: string
                        example: "draft"
                      labels:
                        type: array
                        items:
                          type: string
                        example: ["edm", "api"]
                      targetLists:
                        type: object
                        nullable: true
                        additionalProperties: true
                        description: Saved campaign audience, or null when targeting is still unset.
                      replyProfileId:
                        type: string
                        nullable: true
                        example: "reply_abc123"
                      replyToName:
                        type: string
                        nullable: true
                        example: "Support"
                      replyToEmail:
                        type: string
                        format: email
                        nullable: true
                        example: "support@example.com"
                      ccEmails:
                        type: array
                        nullable: true
                        items:
                          type: string
                          format: email
                        example: ["ops@example.com"]
                      bccEmails:
                        type: array
                        nullable: true
                        items:
                          type: string
                          format: email
                        example: ["archive@example.com"]
                      url:
                        type: string
                        format: uri
                        example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123"
                      previewUrl:
                        type: string
                        format: uri
                        example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
                required:
                  - success
                  - campaign
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                conflicting-reply-to:
                  summary: Both reply-to inputs provided
                  value:
                    success: false
                    error: "Provide either replyProfileId or replyTo, not both. replyProfileId already sets both the Reply-To address and the display name, so send it without replyTo and replyToName. To use a different address or display name, omit replyProfileId and send replyTo (optionally with replyToName) instead."
        "422":
          description: Request body failed schema validation
          content:
            application/json:
              schema:
                type: object
                properties:
                  type:
                    type: string
                    example: "validation"
                  on:
                    type: string
                    example: "body"
                  property:
                    type: string
                    example: "/replyTo"
                  message:
                    type: string
                    example: "Expected string to match 'email' format"
                  summary:
                    type: string
                    example: "Property 'replyTo' should be email"
                  expected:
                    type: object
                    additionalProperties: true
                  found:
                    type: object
                    additionalProperties: true
                  errors:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
              examples:
                invalid-reply-to:
                  summary: Invalid reply-to email
                  value:
                    type: "validation"
                    on: "body"
                    property: "/replyTo"
                    message: "Expected string to match 'email' format"
                    summary: "Property 'replyTo' should be email"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      operationId: deleteCampaign
      summary: Delete campaign
      description: Permanently deletes a campaign. Active campaigns (sending, scheduled, or paused) must be cancelled first.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Campaign deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Campaign is active and must be cancelled first
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /campaigns/{campaignId}/audience:
    get:
      operationId: getCampaignAudience
      summary: Get campaign audience
      description: Resolves the campaign's stored targeting into named lists and segments and returns a recipient count computed at read time. When audience.isUnset is true the campaign has no targeting and scheduling sends to every active subscriber.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Resolved campaign audience
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaignId:
                    type: string
                  campaignName:
                    type: string
                  status:
                    type: string
                  scheduledAt:
                    type: string
                    format: date-time
                    nullable: true
                  audience:
                    $ref: "#/components/schemas/ResolvedCampaignAudience"
                  recipientCount:
                    type: integer
                    description: Subscribers matching the effective targeting right now.
                  targetLists:
                    type: object
                    nullable: true
                    additionalProperties: true
                    description: Raw stored targeting exactly as persisted on the campaign.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /campaigns/{campaignId}/schedule:
    post:
      operationId: scheduleCampaign
      summary: Schedule campaign
      description: >-
        Schedules a draft or already scheduled campaign for a future send time.
        Requires a verified sending domain. Campaigns that require safety review
        are held in waiting_approval and scheduled after a reviewer approves
        them. A waiting_approval result is a normal 200 outcome and is most
        common on new accounts and recently registered sending domains; retrying
        the schedule call does not clear the hold, so branch on campaign.status
        and poll GET /campaigns/{campaignId} instead. See
        https://docs.sequenzy.com/concepts/campaigns#safety-review
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                scheduledAt:
                  type: string
                  format: date-time
                  description: Future send time.
                  example: "2026-06-01T14:00:00Z"
                targetLists:
                  type: object
                  description: 'Optional targeting object. Omit to reuse saved targeting - or, when none is saved, ALL active subscribers. The object is a union discriminated on type: {"type":"all"}, {"type":"lists","listIds":["list_123"]}, {"type":"segment","segmentId":"seg_123"}, {"type":"filtered","filters":[],"filterJoinOperator":"and"}, {"type":"rules","include":[],"exclude":[]}. Mutually exclusive with listIds.'
                  additionalProperties: true
                  example:
                    type: all
                listIds:
                  type: array
                  items:
                    type: string
                  description: Shorthand for sending to one or more lists. Equivalent to `targetLists` `{"type":"lists","listIds":["list_123"]}`. Mutually exclusive with targetLists.
                sendTimeOptimization:
                  type: boolean
                  description: Whether to use send-time optimization.
                  example: false
                spreadOverHours:
                  type: number
                  nullable: true
                  minimum: 1
                  maximum: 72
                  description: Spread delivery over this many hours. When set, spread delivery takes precedence over send-time optimization.
                  example: 6
                sendInRecipientTimezone:
                  type: boolean
                  description: Deliver at scheduledAt's wall-clock time in each recipient's own timezone. Requires scheduledTimezone. Contacts without a stored timezone receive the campaign at scheduledAt itself. Not combinable with recurringInterval or spreadOverHours. Omitting it on a reschedule preserves the campaign's existing setting; send false to turn it off.
                  example: false
                scheduledTimezone:
                  type: string
                  nullable: true
                  maxLength: 64
                  description: IANA timezone the scheduledAt wall-clock time refers to, for example America/New_York. Required with sendInRecipientTimezone.
                  example: "America/New_York"
                recurringInterval:
                  type: string
                  nullable: true
                  enum: [weekly, monthly]
                  description: Repeat the campaign on a cadence starting at scheduledAt. The campaign becomes a recurring template - each run is duplicated and sent automatically, re-evaluating audience membership every time. Omit or send null for a one-shot send; scheduling again without it stops the recurrence.
                  example: monthly
              required:
                - scheduledAt
            examples:
              schedule-all:
                summary: Schedule all active subscribers
                value:
                  scheduledAt: "2026-06-01T14:00:00Z"
                  targetLists:
                    type: all
              schedule-list:
                summary: Schedule a list
                value:
                  scheduledAt: "2026-06-01T14:00:00Z"
                  targetLists:
                    type: lists
                    listIds: ["list_abc123"]
              schedule-list-shorthand:
                summary: Schedule a list via the listIds shorthand
                value:
                  scheduledAt: "2026-06-01T14:00:00Z"
                  listIds: ["list_abc123"]
              schedule-recurring:
                summary: Repeat monthly to a segment
                value:
                  scheduledAt: "2026-06-01T14:00:00Z"
                  targetLists:
                    type: segment
                    segmentId: "seg_abc123"
                  recurringInterval: monthly
              schedule-recipient-timezone:
                summary: Deliver at 8pm in each recipient's timezone
                value:
                  scheduledAt: "2026-06-02T00:00:00Z"
                  sendInRecipientTimezone: true
                  scheduledTimezone: "America/New_York"
      responses:
        "200":
          description: Campaign scheduled successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    description: Scheduling result message. If the campaign requires review, it is held in waiting_approval instead of queueing a send job.
                    example: "Campaign scheduled for 2026-06-01T14:00:00.000Z"
                  scheduledAt:
                    type: string
                    format: date-time
                  jobId:
                    type: string
                    nullable: true
                    example: "mock-job-id"
                  previewUrl:
                    type: string
                    format: uri
                    example: "https://sequenzy.com/dashboard/company/comp_abc123/campaign/camp_abc123?step=review"
                  campaign:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "camp_abc123"
                      name:
                        type: string
                        example: "April Launch"
                      subject:
                        type: string
                        example: "A quick update"
                      status:
                        type: string
                        enum: ["scheduled", "waiting_approval"]
                        description: >-
                          `scheduled` when the send job was queued.
                          `waiting_approval` when the campaign was held for
                          safety review - common on new accounts and recently
                          registered sending domains - in which case no send job
                          is queued and jobId is omitted.
                        example: "scheduled"
                      labels:
                        type: array
                        items:
                          type: string
                      scheduledAt:
                        type: string
                        format: date-time
                      sendInRecipientTimezone:
                        type: boolean
                        description: Whether delivery follows each recipient's local wall clock.
                      scheduledTimezone:
                        type: string
                        nullable: true
                        maxLength: 64
                        description: IANA timezone that anchors scheduledAt's wall-clock time.
                        example: "America/New_York"
                      url:
                        type: string
                        format: uri
                      previewUrl:
                        type: string
                        format: uri
                required:
                  - success
                  - scheduledAt
                  - previewUrl
                  - campaign
        "400":
          description: Validation error - invalid schedule time, unverified sending domain, empty campaign content (missing subject or content blocks), or an audience without any include rules
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign or sender profile not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Campaign is no longer editable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/render:
    post:
      operationId: renderCampaign
      summary: Render campaign HTML
      description: >-
        Render a campaign to the exact email-safe HTML that would be sent, for embedding a visual preview.
        Read-only: this never sends or modifies anything, and uses POST only so
        personalization input can travel in a request body.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenderEmailRequest"
      responses:
        "200":
          description: Email rendered successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenderEmailResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: >-
            API key is missing the required read scope, or is missing
            subscribers:read while subscriberId was supplied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/nodes/{nodeId}/render:
    post:
      operationId: renderSequenceStep
      summary: Render sequence step HTML
      description: >-
        Render one sequence email step to the exact email-safe HTML that would be sent, for embedding a visual preview.
        Read-only: this never sends or modifies anything, and uses POST only so
        personalization input can travel in a request body.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          description: Sequence ID
          schema:
            type: string
        - name: nodeId
          in: path
          required: true
          description: Email step node ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenderEmailRequest"
      responses:
        "200":
          description: Email rendered successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenderEmailResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: >-
            API key is missing the required read scope, or is missing
            subscribers:read while subscriberId was supplied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence or step not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /templates/{templateId}/render:
    post:
      operationId: renderTemplate
      summary: Render template HTML
      description: >-
        Render a template to the exact email-safe HTML that would be sent, for embedding a visual preview.
        Read-only: this never sends or modifies anything, and uses POST only so
        personalization input can travel in a request body.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          description: Template ID, transactional email ID, or transactional slug.
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenderEmailRequest"
      responses:
        "200":
          description: Email rendered successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RenderEmailResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: >-
            API key is missing the required read scope, or is missing
            subscribers:read while subscriberId was supplied
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/preview-computed-data:
    post:
      operationId: previewCampaignComputedData
      summary: Preview campaign personalized lists
      description: Preview the per-recipient lists that a campaign computes from campaign data.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                subscriberId:
                  type: string
                  description: Existing subscriber ID to use for preview.
                  example: "sub_abc123"
                subscriber:
                  type: object
                  description: Inline subscriber preview data.
                  additionalProperties: false
                  properties:
                    email:
                      type: string
                      format: email
                    firstName:
                      type: string
                      nullable: true
                    lastName:
                      type: string
                      nullable: true
                    customAttributes:
                      type: object
                      additionalProperties: true
                  required:
                    - email
            examples:
              preview-inline-subscriber:
                summary: Preview with inline subscriber attributes
                value:
                  subscriber:
                    email: "anna@example.com"
                    customAttributes:
                      region: "Auckland"
                      interests: ["theatre", "arts"]
      responses:
        "200":
          description: Computed data previewed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  variables:
                    type: object
                    additionalProperties: true
                  lists:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        items:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                        exposures:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                required:
                  - success
                  - variables
                  - lists
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Request body failed schema validation
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /campaigns/{campaignId}/test:
    post:
      operationId: sendCampaignTest
      summary: Send campaign test
      description: Queues a test send for a campaign and returns a durable email send ID for delivery-status inspection.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                to:
                  type: string
                  format: email
                  description: Test recipient email address.
              required:
                - to
      responses:
        "200":
          description: Test send accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Test email queued for you@example.com"
                  campaignId:
                    type: string
                  recipientEmail:
                    type: string
                    format: email
                  emailSendId:
                    type: string
                    description: Durable delivery ID. Use this with GET /email-sends/{emailSendId}.
                  jobId:
                    type: string
                    deprecated: true
                    description: Legacy queue identifier retained for response compatibility.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/unschedule:
    post:
      operationId: unscheduleCampaign
      summary: Unschedule campaign
      description: Removes the pending send for a scheduled campaign and returns it to an editable draft. Recurrence is stopped, and the campaign can be edited and scheduled again.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Campaign returned to draft
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    $ref: "#/components/schemas/CampaignDetail"
        "400":
          description: Campaign is not scheduled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The authenticated member has view-only access to the company
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Campaign status changed concurrently
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/cancel:
    post:
      operationId: cancelCampaign
      summary: Cancel campaign
      description: Cancels a sending, paused, scheduled, waiting_approval, or rejected campaign and removes any pending send jobs.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Campaign cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    $ref: "#/components/schemas/CampaignDetail"
        "400":
          description: Campaign is not in a cancellable status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Campaign status changed concurrently
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/pause:
    post:
      operationId: pauseCampaign
      summary: Pause campaign
      description: Pauses a campaign that is currently sending. In-progress chunk workers stop and remaining recipients are held until resume.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Campaign paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    $ref: "#/components/schemas/CampaignDetail"
        "400":
          description: Campaign is not currently sending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Campaign status changed concurrently
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Campaign could not be paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/share-link:
    post:
      operationId: createCampaignShareLink
      summary: Create share link
      description: Creates (or fetches) 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. Email campaigns only.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Share link active
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  shareUrl:
                    type: string
                    description: Public anonymized view-in-browser URL.
                    example: "https://sequenzy.com/view/campaign/8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s"
                  shareToken:
                    type: string
                    description: Capability token embedded in the URL.
                  created:
                    type: boolean
                    description: False when an already-active link was returned instead of minted.
        "400":
          description: SMS campaigns have no browser view to share
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: revokeCampaignShareLink
      summary: Revoke share link
      description: Revokes the campaign's public view-in-browser link. The shared URL returns 404 immediately; sharing again later mints a different URL. Returns revoked=false when no link was active.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Share link revoked (or none was active)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  revoked:
                    type: boolean
                    description: False when the campaign had no active public link.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /campaigns/{campaignId}/resume:
    post:
      operationId: resumeCampaign
      summary: Resume campaign
      description: Resumes a paused campaign. Sending continues with remaining recipients, including A/B test phases when the campaign has a linked test.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                spreadOverHours:
                  type: integer
                  nullable: true
                  minimum: 1
                  maximum: 72
                  description: Spread remaining delivery over this many hours. Pass null to clear an existing spread.
                  example: 6
      responses:
        "200":
          description: Campaign resumed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    $ref: "#/components/schemas/CampaignDetail"
        "400":
          description: Campaign is not paused or spreadOverHours is invalid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Campaign status changed concurrently
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/duplicate:
    post:
      operationId: duplicateCampaign
      summary: Duplicate campaign
      description: Creates a draft copy of a campaign. Optionally copies the campaign's A/B test or duplicates a single variant as a plain campaign.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                mode:
                  type: string
                  enum: [campaign, ab_test, variant]
                  default: campaign
                  description: campaign copies the campaign email, ab_test also copies the linked A/B test and variants, variant copies one variant's content as a plain campaign.
                variantId:
                  type: string
                  description: Variant ID to copy. Required when mode is variant.
      responses:
        "200":
          description: Campaign duplicated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    allOf:
                      - $ref: "#/components/schemas/CampaignDetail"
                      - type: object
                        properties:
                          emailId:
                            type: string
                            description: ID of the duplicated email.
        "400":
          description: Missing or invalid variantId for variant mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign, campaign email, or A/B test not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Campaign could not be duplicated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /campaigns/{campaignId}/resend-to-non-openers:
    post:
      operationId: resendCampaignToNonOpeners
      summary: Resend campaign to non-openers
      description: Creates a draft that resends a sent campaign to everyone in the same audience who didn't open it. Reuses the original audience plus a "didn't open this campaign" rule. Only available 6 hours after the campaign finishes sending, and never for imported already-sent campaigns, which have no opens in Sequenzy. The draft must be scheduled or sent separately.
      tags:
        - Campaigns
      parameters:
        - name: campaignId
          in: path
          required: true
          description: Campaign ID
          schema:
            type: string
      responses:
        "200":
          description: Non-opener resend draft created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  campaign:
                    allOf:
                      - $ref: "#/components/schemas/CampaignDetail"
                      - type: object
                        properties:
                          emailId:
                            type: string
                            description: ID of the new resend email.
                  estimatedNonOpenerCount:
                    type: integer
                    description: Estimated number of subscribers who haven't opened the original campaign.
        "400":
          description: Campaign hasn't been sent, it's been less than 6 hours since it finished sending, or it is an imported already-sent campaign
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign or campaign email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Resend draft could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Landing Page Operations
  # ============================================================================
  /landing-pages:
    get:
      operationId: listLandingPages
      summary: List landing pages
      description: Lists landing pages for the authenticated company.
      tags:
        - Landing Pages
      responses:
        "200":
          description: Landing pages returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  companyId:
                    type: string
                    example: "comp_abc123"
                  landingPages:
                    type: array
                    items:
                      $ref: "#/components/schemas/LandingPageSummary"
                  url:
                    type: string
                    format: uri
                    example: "https://sequenzy.com/dashboard/company/comp_abc123/landing-pages"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createLandingPage
      summary: Create landing page
      description: Creates a draft landing page from default template content or supplied builder JSON.
      tags:
        - Landing Pages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Landing page name.
                slug:
                  type: string
                  maxLength: 255
                  description: URL slug. It is normalized and made unique for the company.
                template:
                  type: string
                  enum:
                    [
                      from-scratch,
                      waitlist,
                      lead-magnet,
                      launch,
                      demo-request,
                      webinar,
                      newsletter,
                      product-hunt,
                      pricing-offer,
                      agency-lead-gen,
                      feature-announcement,
                    ]
                  description: Template key used when content is omitted.
                content:
                  $ref: "#/components/schemas/LandingPageContent"
            examples:
              create-from-template:
                summary: Create from template
                value:
                  name: "Product Waitlist"
                  slug: "product-waitlist"
                  template: "waitlist"
      responses:
        "200":
          description: Landing page created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
                  message:
                    type: string
                    example: "Landing page created as a draft. Use publish_landing_page when it is ready to go live."
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Landing page could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /landing-pages/domain:
    get:
      operationId: getLandingPageDomain
      summary: Get landing page domain settings
      description: Returns the custom landing page domain settings for the authenticated company.
      tags:
        - Landing Pages
      responses:
        "200":
          description: Domain settings returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: connectLandingPageDomain
      summary: Connect landing page domain
      description: Connects or replaces the custom domain for published landing pages.
      tags:
        - Landing Pages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                domain:
                  type: string
                  description: Custom landing page domain.
                  example: "pages.example.com"
              required:
                - domain
      responses:
        "200":
          description: Landing page domain connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
                  message:
                    type: string
                    example: "Landing page domain connected. Add a CNAME record pointing to pages.sequenzydns.com, then verify the domain."
        "400":
          description: Invalid domain or provisioning error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Domain conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Domain could not be connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      operationId: updateLandingPageDomainSettings
      summary: Update landing page domain settings
      description: Replaces the custom landing page domain, verifies the current domain, or both.
      tags:
        - Landing Pages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                domain:
                  type: string
                  description: Replacement custom landing page domain.
                  example: "pages.example.com"
                verify:
                  type: boolean
                  description: Check DNS and SSL status for the current domain.
                  example: true
      responses:
        "200":
          description: Domain settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
        "400":
          description: Missing input, invalid domain, or verification error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Domain conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Domain settings could not be updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /landing-pages/domain/verify:
    post:
      operationId: verifyLandingPageDomain
      summary: Verify landing page domain
      description: Checks DNS and SSL status for the current custom landing page domain.
      tags:
        - Landing Pages
      responses:
        "200":
          description: Domain verification status returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
        "400":
          description: No domain configured or verification error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Domain could not be verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /landing-pages/{landingPageId}/domain:
    get:
      operationId: getLandingPageDedicatedDomain
      summary: Get a dedicated page domain
      description: Returns the domain assigned only to this landing page plus its workspace fallback.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Page domain settings returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: connectLandingPageDedicatedDomain
      summary: Connect a dedicated page domain
      description: Assigns one hostname to one landing page. The page opens at the hostname root, while existing workspace and Sequenzy URLs remain available.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                domain:
                  type: string
                  example: "offer.example.com"
              required:
                - domain
      responses:
        "200":
          description: Dedicated page domain connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
                  message:
                    type: string
        "409":
          description: Domain conflict or an existing page domain must be removed first
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: removeLandingPageDedicatedDomain
      summary: Remove a dedicated page domain
      description: Removes only the page-specific hostname. Workspace and Sequenzy fallback URLs remain available.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Dedicated page domain removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"

  /landing-pages/{landingPageId}/domain/verify:
    post:
      operationId: verifyLandingPageDedicatedDomain
      summary: Verify a dedicated page domain
      description: Checks DNS and SSL status for the hostname assigned to this landing page.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Dedicated page domain verification returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  domain:
                    $ref: "#/components/schemas/LandingPageDomain"
        "400":
          description: No dedicated domain is configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /landing-pages/{landingPageId}:
    get:
      operationId: getLandingPage
      summary: Get landing page
      description: Returns one landing page with builder content and public URLs.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID
          schema:
            type: string
      responses:
        "200":
          description: Landing page returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateLandingPage
      summary: Update landing page
      description: Updates a landing page name, slug, or builder content.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                slug:
                  type: string
                  maxLength: 255
                content:
                  $ref: "#/components/schemas/LandingPageContent"
      responses:
        "200":
          description: Landing page updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
        "400":
          description: Missing input or validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      operationId: deleteLandingPage
      summary: Delete landing page
      description: Deletes a landing page.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID
          schema:
            type: string
      responses:
        "200":
          description: Landing page deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPageId:
                    type: string
                    example: "lp_abc123"
                  message:
                    type: string
                    example: "Landing page deleted."
        "400":
          description: Published landing pages must be unpublished before deletion
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /landing-pages/{landingPageId}/duplicate:
    post:
      operationId: duplicateLandingPage
      summary: Duplicate landing page
      description: >-
        Copies a landing page into a new draft with its own slug, views, and
        conversions. The original keeps its published URL and stats.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID to copy
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Name for the copy. Defaults to the original name with a "(copy)" suffix.
                slug:
                  type: string
                  maxLength: 255
                  description: Slug for the copy. Normalized and made unique within the company.
      responses:
        "200":
          description: Landing page duplicated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
                  message:
                    type: string
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /landing-pages/{landingPageId}/publish:
    post:
      operationId: publishLandingPage
      summary: Publish landing page
      description: Publishes a landing page and optionally updates name, slug, or content first.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                slug:
                  type: string
                  maxLength: 255
                content:
                  $ref: "#/components/schemas/LandingPageContent"
      responses:
        "200":
          description: Landing page published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /landing-pages/{landingPageId}/unpublish:
    post:
      operationId: unpublishLandingPage
      summary: Unpublish landing page
      description: Returns a landing page to draft status and optionally updates name, slug, or content first.
      tags:
        - Landing Pages
      parameters:
        - name: landingPageId
          in: path
          required: true
          description: Landing page ID
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                slug:
                  type: string
                  maxLength: 255
                content:
                  $ref: "#/components/schemas/LandingPageContent"
      responses:
        "200":
          description: Landing page unpublished
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  landingPage:
                    $ref: "#/components/schemas/LandingPageSummary"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Landing page not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Sequences
  # ============================================================================
  /sequences:
    get:
      operationId: listSequences
      summary: List sequences
      description: Returns filtered, paginated automation sequences for the authenticated company.
      tags:
        - Sequences
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/SequenceStatus"
        - name: search
          in: query
          schema:
            type: string
          description: Case-insensitive name or description search.
        - name: labels
          in: query
          schema:
            type: string
          description: Comma-separated dashboard label names. The label alias is also accepted.
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Page size, up to 100. When limit and offset are both omitted, every sequence is returned.
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Sequences retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  sequences:
                    type: array
                    items:
                      $ref: "#/components/schemas/SequenceSummary"
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                        nullable: true
                        description: Applied page size, or null when the unpaginated default returned every sequence.
                      offset:
                        type: integer
                      count:
                        type: integer
                      total:
                        type: integer
                      hasMore:
                        type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createSequence
      summary: Create sequence
      description: Creates a draft automation sequence using AI-generated content, explicit email/action steps, or a blank trigger-to-completion graph when both are omitted. Discount action steps dynamically generate Stripe or Shopify codes that later emails can reference with discount merge tags.
      tags:
        - Sequences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceCreateRequest"
            examples:
              blank-draft:
                summary: Blank dashboard-compatible draft
                value:
                  name: "Cancellation feedback"
              ai-generated:
                summary: AI-generated sequence
                value:
                  name: "Onboarding Sequence"
                  trigger: "contact_added"
                  stopCondition:
                    type: "has_tag"
                    value: "customer"
                  sendingWindow:
                    enabled: true
                    timezone: "Europe/Kiev"
                    startTime: "08:00"
                    endTime: "20:00"
                    days:
                      ["monday", "tuesday", "wednesday", "thursday", "friday"]
                  goal: "Welcome new users and help them get started"
                  emailCount: 5
                  durationDays: 14
              explicit-steps:
                summary: Explicit email steps
                value:
                  name: "Welcome Sequence"
                  trigger: "contact_added"
                  steps:
                    - subject: "Welcome to Acme"
                      html: "<h1>Welcome</h1><p>Thanks for joining.</p>"
                    - subject: "Quick tips"
                      delay:
                        days: 1
                      html: "<p>Here are three quick tips.</p>"
              discount-step:
                summary: Explicit discount action and email step
                value:
                  name: "Win-back Discount"
                  trigger: "tag_added"
                  tagName: "cancelled"
                  steps:
                    - type: "create_discount"
                      discount:
                        label: "Create win-back discount"
                        provider: "stripe"
                        discountType: "percent"
                        percentOff: 20
                        duration: "once"
                        appliesToAllPlans: true
                        maxRedemptions: 1
                        lockToSubscriber: true
                        codePrefix: "WINBACK"
                    - subject: "Come back with {{discount.code}}"
                      delay:
                        days: 1
                      html: "<p>Use {{discount.code}} for {{discount.percentOff}}% off.</p>"
              event-triggered:
                summary: Event-triggered sequence
                value:
                  name: "Activation Follow-up"
                  trigger: "event_received"
                  eventName: "onboarding.started"
                  steps:
                    - subject: "Finish setting up {{event.workspaceName}}"
                      html: "<p>Your workspace is ready.</p>"
              product-scoped-purchase:
                summary: Purchase sequence scoped to one product
                value:
                  name: "Ebook Delivery"
                  trigger: "event_received"
                  eventName: "saas.purchase"
                  propertyFilters:
                    - path: "productIds"
                      operator: "equals"
                      value: "prod_ABC123"
                  steps:
                    - subject: "Your download is ready"
                      html: '<p><a href="{{event.download.url}}">Download {{event.download.name}}</a></p>'
              inactivity:
                summary: Inactivity sequence
                value:
                  name: "Login Win-back"
                  trigger: "inactivity"
                  eventName: "app.login"
                  inactiveDays: 14
                  inactivityBaseline: "sequence_created_at"
                  goal: "Bring inactive users back into the app"
      responses:
        "200":
          description: Sequence created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceCreateResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}:
    get:
      operationId: getSequence
      summary: Get sequence
      description: Returns sequence metadata, nodes, and editable email steps.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  sequence:
                    $ref: "#/components/schemas/SequenceDetails"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateSequence
      summary: Update sequence
      description: Updates sequence settings and content, inserts linear or branching steps, or performs revision-guarded graph edits.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceUpdateRequest"
            examples:
              rename:
                summary: Rename sequence
                value:
                  name: "Updated Welcome Sequence"
              pause-new-enrollments:
                summary: Stop new enrollments while current recipients continue
                value:
                  enrollmentPaused: true
              resume-new-enrollments:
                summary: Resume new enrollments
                value:
                  enrollmentPaused: false
              replace-trigger:
                summary: Atomically replace the trigger with an inbound webhook
                value:
                  trigger: "inbound_webhook"
                  eventName: "summit.cancellation_feedback"
                  customIntegration:
                    name: "HeySummit"
                    setupInstructions: "Add the generated URL as an event webhook."
                    samplePayload:
                      attendee:
                        email: "person@example.com"
                    fieldMapping:
                      email: "attendee.email"
                  confirmLiveChange: true
              update-email:
                summary: Update first email by position
                value:
                  emails:
                    - subject: "Welcome to Acme"
                      html: "<h1>Welcome</h1><p>Here is your first step.</p>"
              update-email-by-id:
                summary: Update a specific email step
                value:
                  emails:
                    - nodeId: "node_abc123"
                      name: "Welcome Sequence - Email 1"
                      subject: "Welcome to Acme"
                      blocks:
                        - id: "body"
                          type: "text"
                          content: "<p>Updated body.</p>"
                          variant: "paragraph"
              insert-linear-step:
                summary: Insert a new linear email step after an existing node
                value:
                  confirmStructuralChange: true
                  insertSteps:
                    afterNodeId: "node_migration_email"
                    steps:
                      - name: "Migration check-in"
                        delay:
                          days: 1
                        subject: "Need help migrating?"
                        html: "<p>Here is one more migration resource.</p>"
              move-graph-node:
                summary: Move an existing node after another node
                value:
                  confirmStructuralChange: true
                  graphEdit:
                    action: "move_node"
                    expectedRevision: "75afbbfe05c1a96dc4f167eecc80b14113a3b76b47fb1b193492ddcd2b1d3e21"
                    nodeId: "node_follow_up"
                    afterNodeId: "node_welcome_email"
              replace-graph-edges:
                summary: Replace the complete sequence topology
                value:
                  confirmStructuralChange: true
                  graphEdit:
                    action: "replace_edges"
                    expectedRevision: "75afbbfe05c1a96dc4f167eecc80b14113a3b76b47fb1b193492ddcd2b1d3e21"
                    edges:
                      - sourceNodeId: "node_trigger"
                        targetNodeId: "node_welcome_email"
                      - sourceNodeId: "node_welcome_email"
                        targetNodeId: "node_end"
              stop-condition-and-branch:
                summary: Update stop condition and insert a branch
                value:
                  stopCondition:
                    type: "has_tag"
                    value: "customer"
                  branch:
                    afterNodeId: "node_trigger"
                    branches:
                      - conditionType: "link_clicked"
                        linkUrl: "project-invites"
                        activityScope: "previous_email"
                        steps:
                          - type: "delay"
                            delay:
                              days: 1
                          - subject: "Thanks for accepting"
                            html: "<p>Here is your next project step.</p>"
                    elseSteps:
                      - subject: "Reminder: accept your invite"
                        html: "<p>Please accept your project invite.</p>"
              sending-window:
                summary: Restrict email sends to working hours
                value:
                  sendingWindow:
                    enabled: true
                    timezone: "Europe/Kiev"
                    startTime: "08:00"
                    endTime: "20:00"
                    days:
                      ["monday", "tuesday", "wednesday", "thursday", "friday"]
              clear-sending-window:
                summary: Remove the sequence sending window
                value:
                  sendingWindow: null
              bcc-emails:
                summary: BCC team inboxes on every sequence email
                value:
                  bccEmails: ["cs-team@example.com", "founder@example.com"]
              clear-bcc-emails:
                summary: Remove the sequence BCC addresses
                value:
                  bccEmails: null
      responses:
        "200":
          description: Sequence updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
                  sequence:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "seq_abc123"
                      name:
                        type: string
                        example: "Updated Welcome Sequence"
                      status:
                        $ref: "#/components/schemas/SequenceStatus"
                      enrollmentPaused:
                        type: boolean
                        example: true
                      updatedEmailCount:
                        type: number
                        example: 1
                      updatedSmsStepCount:
                        type: number
                        example: 1
                      updatedSubscriberStepCount:
                        type: number
                        example: 1
                      stopCondition:
                        $ref: "#/components/schemas/SequenceStopCondition"
                      sendingWindow:
                        nullable: true
                        allOf:
                          - $ref: "#/components/schemas/SequenceSendingWindow"
                      bccEmails:
                        type: array
                        nullable: true
                        items:
                          type: string
                        example: ["cs-team@example.com"]
                      addedBranchNodeId:
                        type: string
                        example: "node_branch"
                      addedBranchPathNodeIds:
                        type: object
                        description: >-
                          Created node IDs per branch path, in path order.
                          Directly wired paths are empty arrays. A path's steps
                          are one linear chain, so to nest another branch on a
                          path, send a second update whose branch.afterNodeId is
                          that path's last node ID; the nested paths reconnect
                          to whatever already followed it.
                        additionalProperties:
                          type: array
                          items:
                            type: string
                        example:
                          branch-0: ["node_if_delay", "node_if_email"]
                          else: ["node_else_email"]
                      insertedNodeIds:
                        type: array
                        items:
                          type: string
                        example: ["node_inserted_email"]
                      insertedEmailIds:
                        type: array
                        items:
                          type: string
                        example: ["email_inserted"]
                      insertedEmailCount:
                        type: number
                        example: 1
                      graphEditAction:
                        type: string
                        enum:
                          [
                            move_node,
                            delete_node,
                            duplicate_node,
                            replace_edges,
                          ]
                      movedNodeId:
                        type: string
                        description: Node moved by a move_node edit.
                      deletedNodeId:
                        type: string
                        description: Node deleted by a delete_node edit.
                      duplicatedNodeId:
                        type: string
                        description: New node created by a duplicate_node edit.
                      replacedEdgeCount:
                        type: number
                        description: Number of edges in the committed graph after a structural graph edit.
                        example: 2
                      graphRevision:
                        type: string
                        description: Revision of the committed graph. Use it for the next graphEdit.expectedRevision.
                        example: "75afbbfe05c1a96dc4f167eecc80b14113a3b76b47fb1b193492ddcd2b1d3e21"
                      migratedRecipientCount:
                        type: number
                        description: Recipients moved off deleted steps to the next step and processed immediately.
                        example: 3
                      completedRecipientCount:
                        type: number
                        description: Recipients completed because their deleted step had no next step.
                        example: 0
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      operationId: deleteSequence
      summary: Delete sequence
      description: Deletes a sequence and its automation nodes.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence deleted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceActionResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sequences/{sequenceId}/goals:
    get:
      operationId: listSequenceGoals
      summary: List sequence goals
      description: Lists the conversion goals configured for a sequence.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sequence goals retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  goals:
                    type: array
                    items:
                      $ref: "#/components/schemas/SequenceGoal"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createSequenceGoal
      summary: Create sequence goal
      description: Creates a conversion goal for an event or subscriber attribute change.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/SequenceGoalInput"
                - type: object
                  required: [name]
      responses:
        "200":
          description: Sequence goal created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  goal:
                    $ref: "#/components/schemas/SequenceGoal"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/goals/{goalId}:
    patch:
      operationId: updateSequenceGoal
      summary: Update sequence goal
      description: Replaces the editable configuration for an existing sequence goal.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
        - name: goalId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceGoalInput"
      responses:
        "200":
          description: Sequence goal updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  goal:
                    $ref: "#/components/schemas/SequenceGoal"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence goal not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteSequenceGoal
      summary: Delete sequence goal
      description: Permanently removes a conversion goal from the sequence.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
        - name: goalId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sequence goal deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  goalId:
                    type: string
        "404":
          description: Sequence goal not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sequences/{sequenceId}/inbound-webhook:
    get:
      operationId: getSequenceInboundWebhook
      summary: Get sequence inbound webhook
      description: Returns the endpoint configuration attached to an inbound_webhook trigger.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sequence inbound webhook retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/SequenceInboundWebhook"
        "400":
          description: Sequence is not configured with a valid inbound_webhook trigger
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence or inbound webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: configureSequenceInboundWebhook
      summary: Configure sequence inbound webhook
      description: Creates or updates the endpoint attached to an inbound_webhook trigger. On first setup, omitted fields use catalog/custom integration defaults; on later calls, omitted fields keep their saved values. Use null to clear a saved mapping or sample.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                fieldMapping:
                  nullable: true
                  allOf:
                    - $ref: "#/components/schemas/SequenceInboundWebhookFieldMapping"
                samplePayload:
                  type: object
                  nullable: true
                  additionalProperties: true
      responses:
        "200":
          description: Sequence inbound webhook configured successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/SequenceInboundWebhook"
        "400":
          description: Sequence is not configured with an inbound_webhook trigger
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sequences/{sequenceId}/inbound-webhook/rotate-secret:
    post:
      operationId: rotateSequenceInboundWebhookSecret
      summary: Rotate sequence inbound webhook secret
      description: Immediately invalidates the previous URL and returns the replacement endpoint.
      tags: [Sequences]
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Secret rotated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/SequenceInboundWebhook"
        "400":
          description: Sequence is not configured with a valid inbound_webhook trigger
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence inbound webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/nodes/{nodeId}/test:
    post:
      operationId: sendSequenceTestEmail
      summary: Send a sequence step test
      description: Queues a real test email for one saved action_email sequence step to one or more internal reviewers. action_ab_test steps are not supported; inspect their variants through the A/B test endpoints. The sequence is not activated and no subscribers are enrolled. Returns one durable email send ID per recipient for delivery inspection.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          description: Sequence ID containing the email step.
          schema:
            type: string
        - name: nodeId
          in: path
          required: true
          description: action_email step node ID returned by the sequence detail endpoint. Do not pass an action_ab_test node.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                recipients:
                  type: array
                  minItems: 1
                  maxItems: 10
                  description: Internal reviewer email addresses. Duplicate addresses are sent only once.
                  items:
                    type: string
                    format: email
              required:
                - recipients
      responses:
        "200":
          description: Sequence step test sends accepted
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - sequenceId
                  - nodeId
                  - results
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Queued 2 sequence step test emails
                  sequenceId:
                    type: string
                  nodeId:
                    type: string
                  results:
                    type: array
                    items:
                      type: object
                      required:
                        - recipientEmail
                        - emailSendId
                        - jobId
                      properties:
                        recipientEmail:
                          type: string
                          format: email
                        emailSendId:
                          type: string
                          description: Durable delivery ID. Use this with GET /email-sends/{emailSendId}.
                        jobId:
                          type: string
                          deprecated: true
                          description: Legacy queue identifier retained for diagnostics.
        "400":
          description: The node is not a configured email step or its sending domain is not verified.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The caller cannot send from this company.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence, sequence step, email template, or sender profile not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/enable:
    post:
      operationId: enableSequence
      summary: Enable sequence
      description: Activates a sequence and opens it for new enrollments. If it was paused, held subscribers continue from their current step and due waits are queued gradually.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence enabled successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceActionResponse"
        "400":
          description: Validation error, including missing Stripe connection for discount sequences
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/pause-enrollments:
    post:
      operationId: pauseSequenceEnrollments
      summary: Pause sequence enrollments
      description: Stops new subscribers from entering an active sequence while current recipients continue through the sequence.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence enrollments paused successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceActionResponse"
        "400":
          description: Sequence is not active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/resume-enrollments:
    post:
      operationId: resumeSequenceEnrollments
      summary: Resume sequence enrollments
      description: Reopens new enrollments for an active sequence whose enrollment gate was paused. Use enableSequence for a fully disabled sequence.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence enrollments resumed successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceActionResponse"
        "400":
          description: Sequence is not active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/disable:
    post:
      operationId: disableSequence
      summary: Disable sequence
      description: Pauses a sequence, blocks new enrollments, and holds workflow execution until the sequence is enabled again.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      responses:
        "200":
          description: Sequence disabled successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceActionResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/duplicate:
    post:
      operationId: duplicateSequence
      summary: Duplicate sequence
      description: Creates an independent draft copy of the sequence graph, email templates, and sequence A/B tests.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Optional name for the copy.
      responses:
        "200":
          description: Sequence duplicated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sequence:
                    $ref: "#/components/schemas/SequenceSummary"
                  nodes:
                    type: array
                    items:
                      $ref: "#/components/schemas/SequenceNode"
                  edges:
                    type: array
                    items:
                      $ref: "#/components/schemas/SequenceGraphEdgeInput"
        "400":
          description: Linked email or A/B test content is missing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/archive:
    post:
      operationId: archiveSequence
      summary: Archive sequence
      description: Archives a sequence and stops new enrollments.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sequence archived successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sequence:
                    $ref: "#/components/schemas/SequenceSummary"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/unarchive:
    post:
      operationId: unarchiveSequence
      summary: Restore archived sequence
      description: Restores an archived sequence as a disabled draft for review.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sequence restored successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sequence:
                    $ref: "#/components/schemas/SequenceSummary"
        "404":
          description: Archived sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/enrollments:
    get:
      operationId: listSequenceEnrollments
      summary: List sequence enrollments
      description: Lists the individual contacts enrolled in one sequence, with the node each one is currently sitting on. Defaults to active and waiting enrollments. Use this when sequence stats give you enrollmentCounts and you need the actual subscribers behind a number.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: "Comma-separated enrollment statuses: active, waiting, completed, failed, cancelled. Defaults to active,waiting."
          example: "waiting"
        - name: currentNodeId
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated sequence node IDs. Only enrollments currently sitting on one of these nodes are returned.
          example: "node_wave_1"
        - name: subscriberId
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated subscriber IDs.
          example: "sub_abc123"
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Exact email address to match, case-insensitive.
          example: "customer@example.com"
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum:
              - enrolled_at_desc
              - enrolled_at_asc
              - wait_until_asc
              - wait_until_desc
          description: Result order. Defaults to enrolled_at_desc. Enrollments with no scheduled resume sort last under wait_until ordering.
        - name: stopConditionMatch
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Annotate each returned active or waiting enrollment with whether the sequence stop condition already matches for that contact right now. Stop conditions are re-evaluated when an enrollment next runs a step, not when their event arrives, so a stopped contact keeps reporting waiting until its delay expires. Use this to confirm a stop event registered without waiting the delay out. Caps the page at 100 regardless of limit.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
          description: Enrollments per page. Values above 500 are capped, and above 100 when stopConditionMatch is true.
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Number of enrollments to skip. Page until pagination.hasMore is false.
      responses:
        "200":
          description: Enrollments matching the filters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentListResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sequences/{sequenceId}/enrollments/cancel:
    post:
      operationId: cancelSequenceEnrollments
      summary: Cancel sequence enrollments
      description: Cancels active or waiting enrollments in one sequence. Target every enrollment with cancelAll, a batch with subscriberIds, one contact with subscriberId, or matching stored entry event property values with fieldValues. Bulk cancellation is capped at 1000 enrollments per request; repeat the request while remainingCount is above zero.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceEnrollmentCancelRequest"
            examples:
              cancel-all:
                summary: Cancel every active or waiting enrollment
                value:
                  cancelAll: true
                  dryRun: false
                  reason: "Lifecycle cutover"
              subscriber:
                summary: Cancel one subscriber
                value:
                  subscriberId: "sub_abc123"
                  reason: "Support request"
              subscriber-batch:
                summary: Cancel a batch of subscriber IDs
                value:
                  subscriberIds: ["sub_abc123", "sub_def456"]
                  dryRun: false
              field-values:
                summary: Cancel matching entry field values
                value:
                  fieldPath: "order.id"
                  fieldValues: ["ord_123", "ord_456"]
                  dryRun: false
                  reason: "Order cancelled"
      responses:
        "200":
          description: Enrollment cancellation completed or dry-run matches returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentCancelResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence or subscriber not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/enrollments/move:
    post:
      operationId: moveSequenceEnrollments
      summary: Move sequence enrollments
      description: Releases a bounded batch of contacts off one sequence step and onto another, keeping their existing enrollment, entry event properties, and stop-condition snapshots. Moved contacts become active on the target step immediately. Defaults to a dry run; each call is capped at 500 and is never drained automatically, so repeat the request while remainingCount is above zero. Works while new enrollment is paused, because the contacts are already enrolled.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceEnrollmentMoveRequest"
            examples:
              preview:
                summary: Preview the next batch waiting on a delay
                value:
                  fromNodeId: "node_delay_2"
                  limit: 180
              paced-release:
                summary: Release a tagged wave under a daily cap
                value:
                  fromNodeId: "node_delay_2"
                  targetNodeId: "node_email_3"
                  limit: 180
                  dailyLimit: 500
                  tags: ["wave-3-released"]
                  reason: "Wave 3"
                  dryRun: false
      responses:
        "200":
          description: Enrollments moved, or dry-run matches returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentMoveResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected, or tags requested without the subscribers:tag scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/enrollments/realign-sending-window:
    post:
      operationId: realignSequenceEnrollments
      summary: Realign waiting enrollments to the sending window
      description: Pulls waiting enrollments forward to the start of the sequence sending window on the day they are already scheduled for. Changing a sending window leaves existing waits alone, 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. Defaults to a synchronous dry run; set dryRun false to queue a background apply job, then poll its status endpoint. Each job is capped at 1000 enrollments; when the completed result has hasMore true, pass nextCursor as cursor on the next request.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SequenceEnrollmentRealignRequest"
            examples:
              preview:
                summary: Preview every waiting enrollment
                value: {}
              apply:
                summary: Apply to the whole sequence
                value:
                  dryRun: false
              one-step:
                summary: Apply to one delay step
                value:
                  nodeIds: ["node_week_14"]
                  dryRun: false
      responses:
        "200":
          description: Dry-run preview
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentRealignResponse"
        "202":
          description: Applied realignment queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentRealignQueuedResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected or missing the sequences:enroll scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/enrollments/realign-sending-window/jobs/{jobId}:
    get:
      operationId: getSequenceEnrollmentRealignment
      summary: Get a sequence enrollment realignment job
      description: Returns the state of an applied realignment job. When status is completed, result contains the bounded realignment result and any continuation cursor.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SequenceEnrollmentRealignJobResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected or missing the sequences:enroll scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence or realignment job not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sequences/{sequenceId}/enroll:
    post:
      operationId: enrollSubscribersInSequence
      summary: Enroll subscribers in sequence
      description: Manually enrolls active subscribers into a sequence by email or subscriber ID, starting at the first step or a specific node.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                emails:
                  type: array
                  items:
                    type: string
                    format: email
                  description: Subscriber emails to enroll. Combined with subscriberIds, up to 500 per request.
                subscriberIds:
                  type: array
                  items:
                    type: string
                  description: Subscriber IDs to enroll. Combined with emails, up to 500 per request.
                targetNodeId:
                  type: string
                  description: Node to start enrollment at. Defaults to the first step after the trigger. Cannot be a trigger node.
      responses:
        "200":
          description: Enrollment completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  enrolled:
                    type: integer
                    description: Number of subscribers enrolled.
                  skipped:
                    type: integer
                    description: Subscribers skipped because they are inactive or already enrolled.
                  notFound:
                    type: array
                    items:
                      type: string
                    description: Emails that did not match any subscriber.
                  targetNodeId:
                    type: string
                    description: Node where enrollment starts.
                  scheduledFor:
                    type: string
                    format: date-time
                    description: When enrolled subscribers process their first step.
        "400":
          description: Validation error, enrollment closed, or invalid target node
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sequences/{sequenceId}/stats:
    get:
      operationId: getSequenceStats
      summary: Get sequence stats
      description: Returns aggregated engagement metrics plus a live active/waiting enrollment breakdown by current node for a specific sequence. This is an alias for /metrics/sequences/{sequenceId}.
      tags:
        - Sequences
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence ID
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  sequenceId:
                    type: string
                    example: "seq_abc123"
                  period:
                    type: string
                    description: "Echoed back when `period` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  enrollmentCounts:
                    $ref: "#/components/schemas/SequenceEnrollmentCounts"
                  enrollmentSkipped:
                    type: object
                    description: "Trigger matches where the contact could not be enrolled because they are unsubscribed or bounced. Defaults to the last 30 days when no explicit time range is provided."
                    properties:
                      count:
                        type: integer
                        description: "Total skipped enrollments in the window"
                      byReason:
                        type: object
                        description: "Skip counts keyed by reason (unsubscribed, bounced)"
                        additionalProperties:
                          type: integer
                  steps:
                    type: array
                    description: Per-email-step metrics, ordered by position in the sequence.
                    items:
                      type: object
                      properties:
                        step:
                          type: integer
                          description: Step number.
                        nodeId:
                          type: string
                          description: Node ID for this email step.
                        subject:
                          type: string
                          nullable: true
                          description: Email subject line.
                        stats:
                          $ref: "#/components/schemas/EngagementStats"
                        failedCount:
                          type: integer
                          description: Number of subscribers that failed at this step.
                        failedSubscribers:
                          type: array
                          description: Up to 20 most recent failed subscribers for this step.
                          items:
                            type: object
                            properties:
                              subscriberId:
                                type: string
                              email:
                                type: string
                                nullable: true
                              failedAt:
                                type: string
                                format: date-time
                              failedReason:
                                type: string
                                nullable: true
                  recommendations:
                    $ref: "#/components/schemas/RecommendationMetrics"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # A/B Test Operations
  # ============================================================================
  /ab-tests:
    get:
      operationId: listAbTests
      summary: List A/B tests
      description: Lists A/B tests and variants for the authenticated company, optionally filtered by sequence.
      tags:
        - A/B Tests
      parameters:
        - name: sequenceId
          in: query
          required: false
          schema:
            type: string
          description: Optional sequence ID filter for automation A/B tests.
      responses:
        "200":
          description: A/B tests returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTests:
                    type: array
                    items:
                      $ref: "#/components/schemas/ABTest"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createAbTest
      summary: Create A/B test
      description: Creates a draft campaign A/B test or converts a sequence email node to action_ab_test. Provide exactly one owner. Variant A is copied into an independent email for sequences; sequence conversions require at least one extra variant.
      tags:
        - A/B Tests
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                campaignId:
                  type: string
                  description: Campaign to attach the test to. Must be in draft or rejected status. Mutually exclusive with automationNodeId.
                automationNodeId:
                  type: string
                  description: Sequence action_email node to convert. Mutually exclusive with campaignId.
                confirmLiveChange:
                  type: boolean
                  description: Must be true when converting an email node in an active sequence.
                name:
                  type: string
                  description: Test name. Defaults to "A/B Test for <campaign name>".
                testPercentage:
                  type: integer
                  minimum: 5
                  maximum: 50
                  default: 20
                  description: Campaign-only share of the audience that receives test sends. Sequence tests use winnerThreshold.
                testDurationMinutes:
                  type: integer
                  minimum: 15
                  maximum: 1440
                  default: 240
                  description: Campaign-only duration before winner selection. Sequence tests select after winnerThreshold recipients.
                winnerCriteria:
                  type: string
                  enum: [open_rate, click_rate]
                  default: open_rate
                  description: Metric used to pick the winner. For sequence tests, an explicit value overrides the testType default.
                testType:
                  type: string
                  enum: [subject, content]
                  default: content
                  description: Sequence variant strategy. Subject defaults to open_rate and content defaults to click_rate unless winnerCriteria is explicit.
                winnerThreshold:
                  type: integer
                  minimum: 10
                  maximum: 1000
                  default: 100
                  description: Number of sequence recipients in the test sample.
                variants:
                  type: array
                  description: Extra variants beyond the control. Required (min 1) when converting with automationNodeId. Total variants cannot exceed 5.
                  items:
                    type: object
                    properties:
                      subject:
                        type: string
                      previewText:
                        type: string
                      blocks:
                        type: array
                        items:
                          $ref: "#/components/schemas/EmailBlock"
                    required:
                      - subject
      responses:
        "200":
          description: A/B test created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTest:
                    $ref: "#/components/schemas/ABTest"
                  nodeConfig:
                    type: object
                    additionalProperties: true
                    description: Rewritten action_ab_test node configuration. Present for sequence conversions only.
                  contentEditing:
                    $ref: "#/components/schemas/AbTestContentEditing"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error, invalid owner, or non-draft campaign
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign, sequence node, or email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: A/B test already exists for this campaign or sequence node
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: A/B test could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /ab-tests/{abTestId}:
    get:
      operationId: getAbTest
      summary: Get A/B test
      description: Returns one A/B test with variants and variant localization status.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
      responses:
        "200":
          description: A/B test returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTest:
                    $ref: "#/components/schemas/ABTest"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    patch:
      operationId: updateAbTest
      summary: Update A/B test settings
      description: Updates a draft campaign test or the effective settings for a sequence test. Campaigns use testPercentage and testDurationMinutes; sequences use testType and winnerThreshold. Sequence changes that affect a live or already-used test require confirmLiveChange.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                  maxLength: 255
                testPercentage:
                  type: integer
                  minimum: 5
                  maximum: 50
                  description: Campaign-only test audience percentage.
                testDurationMinutes:
                  type: integer
                  minimum: 15
                  maximum: 1440
                  description: Campaign-only test duration.
                winnerCriteria:
                  type: string
                  enum: [open_rate, click_rate]
                  description: Winner metric for campaign or sequence tests.
                testType:
                  type: string
                  enum: [subject, content]
                  description: Sequence-only variant strategy.
                winnerThreshold:
                  type: integer
                  minimum: 10
                  maximum: 1000
                  description: Sequence-only recipient threshold.
                confirmLiveChange:
                  type: boolean
                  description: Required when sequence settings affect an active test or a test with recorded activity.
      responses:
        "200":
          description: A/B test settings updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTest:
                    $ref: "#/components/schemas/ABTest"
        "400":
          description: Invalid settings, incompatible test kind, immutable campaign, or missing live-change confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Sequence A/B test updates require the sequences:write scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test or sequence node not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteAbTest
      summary: Delete A/B test
      description: Deletes a campaign A/B test and its variants. Running tests cannot be deleted, and the linked campaign must be in draft or rejected status.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
      responses:
        "200":
          description: A/B test deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Running test, non-campaign test, or non-draft campaign
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: A/B test could not be deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /ab-tests/{abTestId}/stats:
    get:
      operationId: getAbTestStats
      summary: Get A/B test stats
      description: Returns aggregate and per-variant engagement stats for an A/B test.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: Optional period filter.
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Custom range start. Requires end.
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Custom range end. Requires start.
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: A/B test stats returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTestId:
                    type: string
                  period:
                    type: string
                  start:
                    type: string
                    format: date-time
                  end:
                    type: string
                    format: date-time
                  stats:
                    type: object
                    additionalProperties: true
                  variants:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        label:
                          type: string
                        subject:
                          type: string
                        isWinner:
                          type: boolean
                        stats:
                          type: object
                          additionalProperties: true
        "400":
          description: Invalid period or date range
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /ab-tests/{abTestId}/restart:
    post:
      operationId: restartAbTest
      summary: Restart A/B test
      description: Starts a new draft sequence A/B test from the selected control variant after a winner has been selected. The new test becomes active after generated variants are ready.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID to restart.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                sourceVariantId:
                  type: string
                  description: Variant ID to use as the new control email. Defaults to the selected winner.
                testType:
                  type: string
                  enum: [subject, content]
                  description: Test type for generated variants.
                winnerThreshold:
                  type: integer
                  minimum: 10
                  maximum: 1000
                  description: Subscribers before selecting a winner.
                variantCount:
                  type: integer
                  minimum: 2
                  maximum: 4
                  description: Total variants including the control.
      responses:
        "200":
          description: A/B test restarted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  previousAbTestId:
                    type: string
                  sourceVariantId:
                    type: string
                  abTest:
                    $ref: "#/components/schemas/ABTest"
                  nodeConfig:
                    type: object
                    additionalProperties: true
        "400":
          description: Validation error or no selected winner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test or selected control email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: A/B test could not be restarted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /ab-tests/{abTestId}/variants:
    post:
      operationId: addAbTestVariant
      summary: Add A/B test variant
      description: Adds a variant to a draft campaign or sequence A/B test. Sequence variants receive an independent email template. The body defaults to the control email when blocks are omitted. Sequence tests whose parent sequence is active require confirmLiveChange.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                subject:
                  type: string
                  description: Variant subject line.
                previewText:
                  type: string
                  description: Variant preview text.
                blocks:
                  type: array
                  items:
                    $ref: "#/components/schemas/EmailBlock"
                  description: Variant body blocks. Defaults to the campaign or sequence control email blocks.
                confirmLiveChange:
                  type: boolean
                  description: Required as true when the A/B test belongs to an active sequence, because new variants immediately enter the live rotation.
              required:
                - subject
      responses:
        "200":
          description: Variant added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTest:
                    $ref: "#/components/schemas/ABTest"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Non-draft test, variant limit reached, missing owner, invalid blocks, or missing live-change confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Variant could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /ab-tests/{abTestId}/variants/{variantId}:
    patch:
      operationId: updateAbTestVariant
      summary: Update A/B test variant
      description: Updates an A/B test variant's subject, preview text, or body content. Campaign variants remain editable only while the test is in draft. Sequence variants can be edited later with confirmLiveChange when the sequence is active, the test is no longer a draft, or the test has recorded activity; earlier sends remain unchanged, so combined results may no longer be accurate.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
        - name: variantId
          in: path
          required: true
          schema:
            type: string
          description: Variant ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/EmailBodyInput"
                - type: object
                  properties:
                    subject:
                      type: string
                    previewText:
                      type: string
                      nullable: true
                    confirmLiveChange:
                      type: boolean
                      description: Required as true when the sequence is active, the test is no longer a draft, or the test has recorded activity. Earlier sends remain unchanged, so combined results may no longer be accurate.
      responses:
        "200":
          description: Variant updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  variant:
                    $ref: "#/components/schemas/ABTestVariant"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error, immutable non-draft campaign A/B test, or missing live-change confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test or variant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Variant could not be updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteAbTestVariant
      summary: Delete A/B test variant
      description: Removes a variant from a draft campaign or sequence A/B test. The control variant A cannot be deleted, and at least 2 variants must remain. Deleting a sequence variant also deletes its dedicated email template; sequence tests whose parent sequence is active require confirmLiveChange.
      tags:
        - A/B Tests
      parameters:
        - name: abTestId
          in: path
          required: true
          schema:
            type: string
          description: A/B test ID.
        - name: variantId
          in: path
          required: true
          schema:
            type: string
          description: Variant ID.
        - name: confirmLiveChange
          in: query
          required: false
          schema:
            type: boolean
          description: Required as true when the A/B test belongs to an active sequence, because deletion immediately changes the live rotation.
      responses:
        "200":
          description: Variant deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  abTest:
                    $ref: "#/components/schemas/ABTest"
        "400":
          description: Non-draft test, control variant, minimum variant count reached, or missing live-change confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: A/B test or variant not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Variant could not be deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Template Operations
  # ============================================================================
  /templates:
    get:
      operationId: listTemplates
      summary: List templates
      description: >-
        Lists saved email templates for the authenticated company, optionally
        filtered by label. Templates are the company's saved email bodies:
        standalone templates plus the bodies behind campaigns and transactional
        emails, so dashboard-designed emails appear here too. A campaign's
        `emailId` points at its entry in this list, and any template ID can be
        passed as `templateId` when creating a campaign. Bodies are kept when
        their campaign or transactional email is deleted. Results are newest
        first and paginated: 50 per page by default, up to 100. Page with
        `offset` while `pagination.hasMore` is true.
      tags:
        - Templates
      parameters:
        - name: label
          in: query
          required: false
          schema:
            type: string
          description: Optional label name filter. Only templates assigned this label are returned.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Templates per page. Values above 100 are clamped to 100.
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Templates to skip before returning results.
      responses:
        "200":
          description: Templates returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  companyId:
                    type: string
                  emailLocalizationConfig:
                    type: object
                    additionalProperties: true
                  templates:
                    type: array
                    items:
                      $ref: "#/components/schemas/TemplateSummary"
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                        example: 50
                      offset:
                        type: integer
                        example: 0
                      count:
                        type: integer
                        description: Templates on this page.
                        example: 50
                      total:
                        type: integer
                        description: Every matching email body.
                        example: 137
                      hasMore:
                        type: boolean
                        example: true
        "400":
          description: Invalid limit or offset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createTemplate
      summary: Create template
      description: Creates a reusable email template from exactly one of prompt, HTML, or Sequenzy blocks.
      tags:
        - Templates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                subject:
                  type: string
                  description: Required with HTML or blocks; optional with prompt, where it overrides the generated subject.
                previewText:
                  type: string
                  nullable: true
                html:
                  type: string
                  description: Raw HTML body. Mutually exclusive with blocks.
                blocks:
                  type: array
                  description: Sequenzy email blocks. Mutually exclusive with html. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
                  items:
                    type: object
                    additionalProperties: true
                prompt:
                  type: string
                  minLength: 1
                  description: Natural-language request for branded native template blocks.
                style:
                  type: string
                  description: Generation style; valid only with prompt.
                tone:
                  type: string
                  description: Generation tone; valid only with prompt.
                labels:
                  type: array
                  description: Label names to assign. Missing labels are created automatically.
                  items:
                    type: string
                label:
                  type: array
                  description: Compatibility alias for labels.
                  items:
                    type: string
              required:
                - name
      responses:
        "200":
          description: Template created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  template:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      subject:
                        type: string
                      labels:
                        type: array
                        items:
                          type: string
                        example: ["edm", "template"]
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Template could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /templates/{templateId}:
    get:
      operationId: getTemplate
      summary: Get template
      description: Returns one email template. Transactional email IDs and slugs are also resolved for compatibility, as is the `emailId` returned by campaign endpoints, so this can read the blocks of an email designed in the dashboard.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: Template ID, transactional email ID, or transactional slug.
      responses:
        "200":
          description: Template returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  template:
                    $ref: "#/components/schemas/TemplateDetail"
                  transactional:
                    type: object
                    nullable: true
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    put:
      operationId: updateTemplate
      summary: Update template
      description: Updates template metadata, labels, or content. Transactional email IDs and slugs are also resolved for compatibility.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: Template ID, transactional email ID, or transactional slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name:
                  type: string
                subject:
                  type: string
                previewText:
                  type: string
                  nullable: true
                  description: Inbox preview text. Send null to clear it.
                html:
                  type: string
                  description: Replacement HTML body. Mutually exclusive with blocks.
                blocks:
                  type: array
                  description: Replacement Sequenzy email blocks. Mutually exclusive with html. Put visual styling under styles; top-level style keys such as backgroundColor, backgroundOpacity, borderColor, borderWidth, and borderRadius are normalized into styles.
                  items:
                    type: object
                    additionalProperties: true
                labels:
                  type: array
                  description: Replacement label names. Send an empty array to clear labels. Missing labels are created automatically.
                  items:
                    type: string
                label:
                  type: array
                  description: Compatibility alias for labels.
                  items:
                    type: string
                updates:
                  description: Unsupported nested update object. Requests using it return a validation error.
      responses:
        "200":
          description: Template updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  template:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      subject:
                        type: string
                      previewText:
                        type: string
                        nullable: true
                      labels:
                        type: array
                        items:
                          type: string
                        example: ["edm", "template"]
                  transactional:
                    type: object
                    nullable: true
                    additionalProperties: true
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Template could not be updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      operationId: deleteTemplate
      summary: Delete template
      description: Deletes an unused email template. Templates used by campaigns, sequences, or transactional emails cannot be deleted.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: Template ID.
      responses:
        "200":
          description: Template deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                  templateId:
                    type: string
        "400":
          description: Template is still in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /templates/{templateId}/share-link:
    post:
      operationId: createTemplateShareLink
      summary: Create share link
      description: Creates (or fetches) the public view-in-browser link for an individual email - a transactional email, a sequence email, or a standalone template. Accepts a template ID or a transactional email's ID or slug; for a sequence email, pass the step's emailId. 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. Campaigns use their own campaign-level share link, which follows the A/B winning variant.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          description: Template ID, transactional email ID, or transactional slug.
          schema:
            type: string
      responses:
        "200":
          description: Share link active
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  shareUrl:
                    type: string
                    description: Public anonymized view-in-browser URL.
                    example: "https://sequenzy.com/view/email/8f2kL9qWm3ZxYv71TnB4c5RdVaEuGh0s"
                  shareToken:
                    type: string
                    description: Capability token embedded in the URL.
                  created:
                    type: boolean
                    description: False when an already-active link was returned instead of minted.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: revokeTemplateShareLink
      summary: Revoke share link
      description: Revokes the email's public view-in-browser link. The shared URL returns 404 immediately; sharing again later mints a different URL. Returns revoked=false when no link was active.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          description: Template ID, transactional email ID, or transactional slug.
          schema:
            type: string
      responses:
        "200":
          description: Share link revoked (or none was active)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  revoked:
                    type: boolean
                    description: False when the email had no active public link.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /templates/{templateId}/localizations/{locale}:
    put:
      operationId: setTemplateLocalization
      summary: Set template localization
      description: Creates or replaces a caller-supplied localized template variant. The locale must be enabled for the company and cannot be its primary locale.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: Template ID, transactional email ID, or transactional slug.
        - name: locale
          in: path
          required: true
          schema:
            type: string
          description: Enabled non-primary locale code such as es or pt-BR.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - subject
              properties:
                subject:
                  type: string
                  description: Localized email subject line.
                previewText:
                  type: string
                  nullable: true
                  description: Optional localized inbox preview text.
                html:
                  type: string
                  description: Localized raw HTML. Provide exactly one of html or blocks.
                blocks:
                  type: array
                  description: Localized Sequenzy email blocks. Provide exactly one of blocks or html.
                  items:
                    $ref: "#/components/schemas/EmailBlock"
      responses:
        "200":
          description: Template localization saved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  templateId:
                    type: string
                  localization:
                    $ref: "#/components/schemas/TemplateLocalization"
                  warnings:
                    $ref: "#/components/schemas/BlockFieldWarnings"
        "400":
          description: Invalid or unsupported locale or content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Template localization could not be saved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /templates/{templateId}/localizations/sync:
    post:
      operationId: syncTemplateLocalizations
      summary: Sync template localizations
      description: Queues AI translation for selected enabled template locales. Omit locales to sync every enabled non-primary locale, even when automatic on-save sync is disabled.
      tags:
        - Templates
      parameters:
        - name: templateId
          in: path
          required: true
          schema:
            type: string
          description: Template ID, transactional email ID, or transactional slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                locales:
                  type: array
                  description: Enabled non-primary locale codes to sync. Omit to sync all of them.
                  items:
                    type: string
      responses:
        "200":
          description: Template localizations queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  templateId:
                    type: string
                  queuedLocales:
                    type: array
                    items:
                      type: string
                  queuedVariantCount:
                    type: integer
        "400":
          description: Invalid locales or no enabled non-primary locales
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /generate/email:
    post:
      operationId: generateEmail
      summary: Generate email
      description: Generates a draft email from scratch as structured editor-compatible blocks. By default, the generated content is wrapped with the company's logo and footer.
      tags:
        - Generation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                  description: What you want the email to say or accomplish.
                  example: "Announce our new analytics dashboard to trial users"
                style:
                  type: string
                  description: Optional style guidance.
                  example: "branded"
                tone:
                  type: string
                  description: Optional tone guidance.
                  example: "friendly"
                applyBranding:
                  type: boolean
                  default: true
                  description: Whether to wrap generated content with the company logo and footer. Set to false to return raw generated content blocks.
                emailType:
                  type: string
                  enum:
                    - marketing
                    - transactional
                  default: marketing
                  description: Email type. Transactional emails include a footer without an unsubscribe link.
      responses:
        "200":
          description: Email generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subject:
                    type: string
                    example: "Meet your new analytics dashboard"
                  previewText:
                    type: string
                    example: "A clearer way to track what matters"
                  blocks:
                    type: array
                    description: Generated editor-compatible blocks, including company logo and footer branding unless applyBranding is false.
                    items:
                      $ref: "#/components/schemas/EmailBlock"
                  message:
                    type: string
                    example: "Generated a draft email."
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /generate/sequence:
    post:
      operationId: generateSequence
      summary: Generate sequence (deprecated)
      description: Deprecated compatibility alias that creates and persists a disabled contact_added sequence draft from a goal. Use POST /sequences for new integrations.
      deprecated: true
      tags:
        - Sequences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - goal
              properties:
                goal:
                  type: string
                  description: Sequence goal or desired subscriber journey.
                  example: "Onboard a new workspace admin"
                name:
                  type: string
                  description: Optional sequence name. Defaults to the normalized goal.
                  example: "Workspace onboarding"
                listId:
                  type: string
                  description: Optional list ID that scopes the contact_added trigger.
                emailCount:
                  type: number
                  minimum: 1
                  maximum: 10
                  description: Number of emails to generate. Defaults to 5. Maximum is 10.
                  example: 3
                durationDays:
                  type: number
                  minimum: 1
                  description: Duration used to space suggested delays. Defaults to 14.
                  example: 7
      responses:
        "200":
          description: Disabled sequence draft created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SequenceCreateResponse"
                  - type: object
                    properties:
                      deprecated:
                        type: boolean
                        const: true
                      deprecationMessage:
                        type: string
                        example: "POST /api/v1/generate/sequence is deprecated. Use POST /api/v1/sequences with goal-based creation."
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Sequence creation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /generate/subjects:
    post:
      operationId: generateSubjectLines
      summary: Generate subject lines
      description: Generates draft subject line variants for a campaign or sequence email.
      tags:
        - Generation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - topic
              properties:
                topic:
                  type: string
                  description: Topic, campaign idea, or context for the subject lines.
                  example: "April product launch"
                count:
                  type: number
                  minimum: 1
                  description: Number of variants to generate. Defaults to 5.
                  example: 5
      responses:
        "200":
          description: Subject lines generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  topic:
                    type: string
                    example: "April product launch"
                  subjects:
                    type: array
                    items:
                      type: string
                    example:
                      - "Meet what we shipped in April"
                      - "Your April product update is here"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /generate/sms:
    post:
      operationId: generateSmsMessages
      summary: Generate SMS messages
      description: Generates draft SMS marketing message variants with per-message encoding and segment counts. Messages exclude opt-out footers and brand prefixes - Sequenzy adds both automatically at send time.
      tags:
        - Generation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
              properties:
                prompt:
                  type: string
                  description: Description of the SMS to generate.
                  example: "Cart reminder with a free-shipping hook"
                count:
                  type: number
                  minimum: 1
                  maximum: 10
                  description: Number of variants to generate. Defaults to 3.
                  example: 3
      responses:
        "200":
          description: SMS messages generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  prompt:
                    type: string
                    example: "Cart reminder with a free-shipping hook"
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        text:
                          type: string
                          example: "Hey {{FIRST_NAME}}, your cart is waiting! Complete your order today and shipping is on us."
                        encoding:
                          type: string
                          enum: [gsm7, ucs2]
                          example: gsm7
                        segments:
                          type: number
                          example: 1
                        characterCount:
                          type: number
                          example: 94
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No company selected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Generation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sms/settings:
    get:
      operationId: getSmsSettings
      summary: Get SMS settings
      description: Returns the company's SMS add-on status, including credit balance, phone numbers, and whether SMS sequence steps will actually send.
      tags:
        - SMS
      responses:
        "200":
          description: SMS settings returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  sms:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                        example: true
                      planEligible:
                        type: boolean
                        example: true
                      creditsBalance:
                        type: number
                        example: 4200
                      brandPrefix:
                        type: string
                        nullable: true
                        example: "Acme"
                      numbers:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              example: "num_abc123"
                            e164:
                              type: string
                              example: "+18885550123"
                            label:
                              type: string
                              nullable: true
                              description: User-set tag ("Marketing", "Support") shown in number pickers.
                              example: "Marketing"
                            brandPrefix:
                              type: string
                              nullable: true
                              description: Per-number brand prefix override; null inherits the account-wide prefix.
                              example: "Acme Support"
                            status:
                              type: string
                              example: active
                      readyToSend:
                        type: boolean
                        example: true
                  message:
                    type: string
                    example: "SMS is enabled and ready to send."
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to load SMS settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /sms/numbers/{numberId}:
    patch:
      operationId: updateSmsNumberLabel
      summary: Update an SMS number
      description: Updates an SMS number's user-facing label and/or its brand prefix override. Omitted fields keep their value; at least one field is required. Requires companies:manage.
      tags:
        - SMS
      parameters:
        - name: numberId
          in: path
          required: true
          schema:
            type: string
          description: SMS number ID returned by Get SMS Settings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  nullable: true
                  maxLength: 100
                  description: Label such as Marketing or Support. Send null to clear it.
                brandPrefix:
                  type: string
                  nullable: true
                  maxLength: 100
                  description: 'Per-number brand prefix override; messages send as "{prefix}: your message". Send null to clear it back to the account-wide prefix.'
      responses:
        "200":
          description: SMS number updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  number:
                    type: object
                    properties:
                      id:
                        type: string
                        example: "num_abc123"
                      label:
                        type: string
                        nullable: true
                        example: "Marketing"
                      brandPrefix:
                        type: string
                        nullable: true
                        example: "Acme Support"
                  message:
                    type: string
                    example: "SMS number updated."
        "400":
          description: No update fields provided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Insufficient permission
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company or SMS number not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /sms/test:
    post:
      operationId: sendTestSms
      summary: Send a test SMS
      description: Sends a real test text message. Test sends charge credits, bypass quiet hours, are excluded from step stats, and are limited to 5 per company per hour. Requires the SMS add-on with a verified number.
      tags:
        - SMS
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - to
              properties:
                to:
                  type: string
                  description: Destination phone number in international E.164 format.
                  example: "+15550100123"
                text:
                  type: string
                  description: Plain-text message body. Provide text or blocks, not both.
                  example: "Test from Sequenzy"
                imageUrls:
                  type: array
                  description: Up to 2 publicly reachable image URLs sent as MMS media (US/CA only).
                  items:
                    type: string
                blocks:
                  type: array
                  description: SMS content blocks (text + image subset). Provide text or blocks, not both.
                  items:
                    type: object
      responses:
        "200":
          description: Test SMS queued successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  smsSendId:
                    type: string
                    example: "sms_abc123"
                  toPhone:
                    type: string
                    example: "+15550100123"
                  message:
                    type: string
                    example: "Test SMS queued to +15550100123. Test sends bypass quiet hours, charge credits, and are excluded from step stats."
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: SMS add-on not enabled, plan not eligible, or number not verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Test send limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Failed to send test SMS
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /metrics:
    get:
      operationId: getMetrics
      summary: Get aggregated email metrics
      description: Returns aggregated email engagement metrics for the specified time period, plus live subscriberCount (every stored contact) and activeSubscriberCount (status=active) as an audience snapshot independent of period. Set emailType=transactional for Send API and transactional SMTP traffic.
      tags:
        - Analytics
      parameters:
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: ["1h", "24h", "7d", "30d", "90d"]
            default: "90d"
          description: Sliding time window. Ignored when start/end are provided.
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: emailType
          in: query
          required: false
          schema:
            type: string
            enum: ["campaign", "transactional", "sequence"]
          description: Structural email type filter. Use transactional for Send API and transactional SMTP traffic.
        - name: mailboxProvider
          in: query
          required: false
          schema:
            type: string
          description: "Recipient mailbox provider filter (e.g. gmail, microsoft, yahoo, icloud). Scopes engagement metrics to recipients of that provider. Provider-filtered responses report replies as 0 (replies cannot be segmented per provider) and omit the commerce forecast."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  period:
                    type: string
                  emailType:
                    type: string
                    enum: ["campaign", "transactional", "sequence"]
                  mailboxProvider:
                    type: string
                    description: "Echoed back when `mailboxProvider` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  subscriberCount:
                    type: integer
                    description: Live count of every stored contact in the company. Independent of the requested period.
                  activeSubscriberCount:
                    type: integer
                    description: Live count of contacts with status=active. Independent of the requested period. May include phone-only contacts without an email address.
                  commerceForecast:
                    description: Optional latest background-computed forecast snapshot. Omitted when emailType is filtered, no snapshot is available, or analytics storage is temporarily unavailable.
                    $ref: "#/components/schemas/CommerceValueForecast"
        "400":
          description: Invalid period or email type parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/emails:
    get:
      operationId: listEmailMetrics
      summary: List per-email metrics
      description: Returns one row per email - each campaign and each sequence email step - with its own delivery funnel, attributed conversions, and revenue. Sequence rows carry sequenceId, automationNodeId, and the step number, so cross-sequence questions such as how many step-4 emails went out are one request instead of one per sequence. Counts come from retained event storage and match the steps array of the sequence metrics endpoint. The totals object covers every matching email rather than the current page.
      tags:
        - Analytics
      parameters:
        - name: emailType
          in: query
          required: false
          schema:
            type: string
            enum: [campaign, sequence]
          description: Restrict to campaigns or sequence emails. Defaults to both. Implied as sequence when sequenceId or step is set, and as campaign when campaignId is set.
        - name: sequenceId
          in: query
          required: false
          schema:
            type: string
            example: seq_abc123,seq_def456
          description: Comma-separated sequence IDs to restrict the breakdown to. Cannot be combined with campaignId or emailType=campaign.
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
            example: camp_abc123,camp_def456
          description: Comma-separated campaign IDs to restrict the breakdown to. Cannot be combined with sequenceId, step, or emailType=sequence.
        - name: step
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: Keep only sequence emails at this 1-based position, counted in graph order per sequence. Cannot be combined with emailType=campaign.
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: ["1h", "24h", "7d", "30d", "90d"]
          description: Sliding time window. Ignored when start/end are provided. Omit both for all-time counts.
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum:
              [
                sent,
                delivered,
                opened,
                clicked,
                openRate,
                clickRate,
                unsubscribed,
                conversions,
                revenue,
                step,
                name,
              ]
            default: sent
          description: Sort field.
        - name: order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort order.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
          description: Emails per page.
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events in engagement metrics.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  period:
                    type: string
                  start:
                    type: string
                    format: date-time
                  end:
                    type: string
                    format: date-time
                  emailType:
                    type: string
                    enum: [campaign, sequence]
                  step:
                    type: integer
                    description: Echo of the step filter, present only when one was requested.
                  sequenceIds:
                    type: array
                    items:
                      type: string
                    description: Echo of the sequence IDs the breakdown was scoped to.
                  sort:
                    type: string
                  order:
                    type: string
                  emails:
                    type: array
                    items:
                      type: object
                      properties:
                        emailType:
                          type: string
                          enum: [campaign, sequence]
                        emailId:
                          type: string
                          description: Campaign ID for campaigns, automation node ID for sequence emails.
                        name:
                          type: string
                        campaignId:
                          type: string
                          nullable: true
                        sequenceId:
                          type: string
                          nullable: true
                        sequenceName:
                          type: string
                          nullable: true
                        automationNodeId:
                          type: string
                          nullable: true
                        step:
                          type: integer
                          nullable: true
                          description: 1-based position of this email in its sequence, or null for campaigns.
                        stats:
                          type: object
                          description: Delivery funnel for this email alone.
                        conversions:
                          type: integer
                        revenueCents:
                          type: integer
                  totals:
                    type: object
                    description: Summed funnel across every matching email, not just this page, plus emails, conversions, and revenueCents.
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /stats:
    get:
      operationId: getStatsLegacy
      summary: Get aggregated email metrics
      description: Backward-compatible alias for `GET /metrics`. Returns aggregated email engagement metrics for the specified time period and supports the same emailType filter.
      tags:
        - Analytics
      parameters:
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: ["1h", "24h", "7d", "30d", "90d"]
            default: "90d"
          description: Sliding time window. Ignored when start/end are provided.
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: emailType
          in: query
          required: false
          schema:
            type: string
            enum: ["campaign", "transactional", "sequence"]
          description: Structural email type filter. Use transactional for Send API and transactional SMTP traffic.
        - name: mailboxProvider
          in: query
          required: false
          schema:
            type: string
          description: "Recipient mailbox provider filter (e.g. gmail, microsoft, yahoo, icloud). Scopes engagement metrics to recipients of that provider. Provider-filtered responses report replies as 0 (replies cannot be segmented per provider) and omit the commerce forecast."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  period:
                    type: string
                  emailType:
                    type: string
                    enum: ["campaign", "transactional", "sequence"]
                  mailboxProvider:
                    type: string
                    description: "Echoed back when `mailboxProvider` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  commerceForecast:
                    description: Optional latest background-computed forecast snapshot. Omitted when emailType is filtered, no snapshot is available, or analytics storage is temporarily unavailable.
                    $ref: "#/components/schemas/CommerceValueForecast"
        "400":
          description: Invalid period or email type parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/transactional/{idOrSlug}:
    get:
      operationId: getTransactionalMetrics
      summary: Get transactional email metrics
      description: Returns aggregate engagement metrics for one saved transactional email selected by ID or slug. Results are all-time unless a period or custom range is supplied.
      tags:
        - Analytics
      parameters:
        - name: idOrSlug
          in: path
          required: true
          schema:
            type: string
          description: Saved transactional email ID or API slug.
        - name: period
          in: query
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: Optional sliding time window. Ignored when start/end are provided.
        - name: start
          in: query
          schema:
            type: string
            format: date-time
          description: Custom range start. Must be used with end.
        - name: end
          in: query
          schema:
            type: string
            format: date-time
          description: Custom range end. Must be used with start; maximum 90 days.
        - name: includeMachineEngagement
          in: query
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Transactional email metrics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TransactionalMetricsResponse"
        "400":
          description: Invalid time range
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Transactional email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /transactional/{idOrSlug}/stats:
    get:
      operationId: getTransactionalMetricsLegacy
      summary: Get transactional email metrics
      description: Backward-compatible alias for `GET /metrics/transactional/{idOrSlug}`.
      tags:
        - Analytics
      parameters:
        - name: idOrSlug
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
        - name: start
          in: query
          schema:
            type: string
            format: date-time
        - name: end
          in: query
          schema:
            type: string
            format: date-time
        - name: includeMachineEngagement
          in: query
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Transactional email metrics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TransactionalMetricsResponse"
        "404":
          description: Transactional email not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/campaigns/{campaignId}:
    get:
      operationId: getCampaignMetrics
      summary: Get campaign metrics
      description: Returns aggregated engagement metrics plus a lifetime per-link click breakdown and lifetime Poll/NPS summaries for a specific campaign. Clicked links and poll summaries are not limited by period/start/end.
      tags:
        - Analytics
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
          description: Campaign ID
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: mailboxProvider
          in: query
          required: false
          schema:
            type: string
          description: "Recipient mailbox provider filter (e.g. gmail, microsoft, yahoo, icloud). Scopes engagement metrics to recipients of that provider. Provider-filtered responses report replies, conversions, and revenue as 0 because those metrics cannot be segmented per provider."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaignId:
                    type: string
                  period:
                    type: string
                    description: "Echoed back when `period` is provided."
                  mailboxProvider:
                    type: string
                    description: "Echoed back when `mailboxProvider` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  clickedLinks:
                    type: array
                    description: "Lifetime per-link click breakdown, most clicked first (top 20). Omitted when the campaign has no tracked link clicks."
                    items:
                      $ref: "#/components/schemas/ClickedLink"
                  polls:
                    type: array
                    description: "Lifetime Poll and NPS summaries. Omitted when the campaign has no responses."
                    items:
                      $ref: "#/components/schemas/PollResultsSummary"
                  recommendations:
                    $ref: "#/components/schemas/RecommendationMetrics"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/campaigns/{campaignId}/events:
    get:
      operationId: listCampaignEvents
      summary: List campaign events
      description: Returns paginated raw email events for a specific campaign. Defaults to delivery events.
      tags:
        - Analytics
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
          description: Campaign ID
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            enum:
              [
                send,
                delivery,
                bounce,
                complaint,
                open,
                click,
                unsubscribe,
                delivery_delay,
                transport_failure,
              ]
          description: Single event type to include. Defaults to delivery when no event type filter is provided.
        - name: eventTypes
          in: query
          required: false
          schema:
            type: string
            example: delivery,click
          description: Comma-separated event types to include. Supported values are send, delivery, bounce, complaint, open, click, unsubscribe, delivery_delay, and transport_failure.
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Events per page (max 500)
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events when requesting engagement event types.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaignId:
                    type: string
                  eventTypes:
                    type: array
                    items:
                      type: string
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailEvent"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/campaigns/{campaignId}/poll-responses:
    get:
      operationId: listCampaignPollResponses
      summary: List campaign poll responses
      description: >-
        Returns one row per respondent per Poll or NPS block in a campaign, newest answer first,
        with the answer, its stored value, the subscriber attribute the answer was saved to, and the
        response time. Only each subscriber's latest answer per block is returned, so counts match the
        `polls` summaries from the campaign metrics endpoint. Multi-select answers list every selected
        option in `answers` and `values`. For a sequence email step, pass the step's automation node ID
        as `campaignId`. Also available at `GET /campaigns/{campaignId}/poll-responses`.
      tags:
        - Analytics
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
          description: Campaign ID, or the automation node ID of a sequence email step
        - name: blockId
          in: query
          required: false
          schema:
            type: string
          description: Restrict results to one poll block. Block IDs come from the `polls` array of the campaign metrics endpoint.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Responses per page (max 500)
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaignId:
                    type: string
                  blockId:
                    type: string
                    description: Present only when a blockId filter was applied.
                  responses:
                    type: array
                    items:
                      $ref: "#/components/schemas/PollResponse"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /campaigns/{campaignId}/poll-responses:
    get:
      operationId: listCampaignPollResponsesLegacy
      summary: List campaign poll responses
      description: Alias for `GET /metrics/campaigns/{campaignId}/poll-responses`.
      tags:
        - Analytics
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
          description: Campaign ID, or the automation node ID of a sequence email step
        - name: blockId
          in: query
          required: false
          schema:
            type: string
          description: Restrict results to one poll block.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Responses per page (max 500)
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaignId:
                    type: string
                  blockId:
                    type: string
                  responses:
                    type: array
                    items:
                      $ref: "#/components/schemas/PollResponse"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /campaigns/{campaignId}/stats:
    get:
      operationId: getCampaignStatsLegacy
      summary: Get campaign metrics
      description: Backward-compatible alias for `GET /metrics/campaigns/{campaignId}`. Returns aggregated engagement metrics plus a lifetime per-link click breakdown and lifetime Poll/NPS summaries for a specific campaign.
      tags:
        - Analytics
      parameters:
        - name: campaignId
          in: path
          required: true
          schema:
            type: string
          description: Campaign ID
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: mailboxProvider
          in: query
          required: false
          schema:
            type: string
          description: "Recipient mailbox provider filter (e.g. gmail, microsoft, yahoo, icloud). Scopes engagement metrics to recipients of that provider. Provider-filtered responses report replies, conversions, and revenue as 0 because those metrics cannot be segmented per provider."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  campaignId:
                    type: string
                  period:
                    type: string
                  mailboxProvider:
                    type: string
                    description: "Echoed back when `mailboxProvider` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  clickedLinks:
                    type: array
                    description: "Lifetime per-link click breakdown, most clicked first (top 20). Omitted when the campaign has no tracked link clicks."
                    items:
                      $ref: "#/components/schemas/ClickedLink"
                  polls:
                    type: array
                    description: "Lifetime Poll and NPS summaries. Omitted when the campaign has no responses."
                    items:
                      $ref: "#/components/schemas/PollResultsSummary"
                  recommendations:
                    $ref: "#/components/schemas/RecommendationMetrics"
        "404":
          description: Campaign not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/sequences/{sequenceId}:
    get:
      operationId: getSequenceMetrics
      summary: Get sequence metrics
      description: Returns aggregated engagement metrics plus a live active/waiting enrollment breakdown by current node for a specific sequence (automation).
      tags:
        - Analytics
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence (automation) ID
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sequenceId:
                    type: string
                  period:
                    type: string
                    description: "Echoed back when `period` is provided."
                  stats:
                    $ref: "#/components/schemas/EngagementStats"
                  enrollmentCounts:
                    $ref: "#/components/schemas/SequenceEnrollmentCounts"
                  enrollmentSkipped:
                    type: object
                    description: "Trigger matches where the contact could not be enrolled because they are unsubscribed or bounced. Defaults to the last 30 days when no explicit time range is provided."
                    properties:
                      count:
                        type: integer
                        description: "Total skipped enrollments in the window"
                      byReason:
                        type: object
                        description: "Skip counts keyed by reason (unsubscribed, bounced)"
                        additionalProperties:
                          type: integer
                  steps:
                    type: array
                    description: "Per-email-step metrics, ordered by position in the sequence"
                    items:
                      type: object
                      properties:
                        step:
                          type: integer
                          description: "Step number (1-indexed)"
                        nodeId:
                          type: string
                          description: "Node ID for this email step"
                        subject:
                          type: string
                          nullable: true
                          description: "Email subject line"
                        stats:
                          $ref: "#/components/schemas/EngagementStats"
                        failedCount:
                          type: integer
                          description: "Number of subscribers that failed at this step"
                        failedSubscribers:
                          type: array
                          description: "Up to 20 most recent failed subscribers for this step"
                          items:
                            type: object
                            properties:
                              subscriberId:
                                type: string
                                description: "Subscriber ID"
                              email:
                                type: string
                                nullable: true
                                description: "Subscriber email captured on the token"
                              failedAt:
                                type: string
                                format: date-time
                                description: "When the token failed"
                              failedReason:
                                type: string
                                nullable: true
                                description: "Stored failure reason for the token"
                  recommendations:
                    $ref: "#/components/schemas/RecommendationMetrics"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/sequences/{sequenceId}/events:
    get:
      operationId: listSequenceEvents
      summary: List sequence events
      description: Returns paginated raw email events for every email step in a sequence, or for one step via automationNodeId. Defaults to delivery events. This is the per-recipient stream; for per-step totals read the steps array of the sequence metrics endpoint or GET /metrics/emails.
      tags:
        - Analytics
      parameters:
        - name: sequenceId
          in: path
          required: true
          schema:
            type: string
          description: Sequence (automation) ID
        - name: automationNodeId
          in: query
          required: false
          schema:
            type: string
          description: Scope the stream to one email step of this sequence. Take the node ID from the steps array of the sequence metrics endpoint. A node that is not an email step of this sequence returns 400.
        - name: eventType
          in: query
          required: false
          schema:
            type: string
            enum:
              [
                send,
                delivery,
                bounce,
                complaint,
                open,
                click,
                unsubscribe,
                delivery_delay,
                transport_failure,
              ]
          description: Single event type to include. Defaults to delivery when no event type filter is provided.
        - name: eventTypes
          in: query
          required: false
          schema:
            type: string
            example: delivery,open,click
          description: Comma-separated event types to include. Supported values are send, delivery, bounce, complaint, open, click, unsubscribe, delivery_delay, and transport_failure.
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: [1h, 24h, 7d, 30d, 90d]
          description: "Sliding time window. Ignored when `start` and `end` are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Events per page (max 500)
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events when requesting engagement event types.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sequenceId:
                    type: string
                  automationNodeIds:
                    type: array
                    items:
                      type: string
                  eventTypes:
                    type: array
                    items:
                      type: string
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/EmailEvent"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /metrics/recipients:
    get:
      operationId: getRecipients
      summary: Get recipient engagement
      description: Returns a paginated list of recipients with their open, click, and unsubscribe events. Use this to sync engagement data to your own database.
      tags:
        - Analytics
      parameters:
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Filter to a single recipient by email address
        - name: campaignId
          in: query
          required: false
          schema:
            type: string
          description: Filter to recipients of a specific campaign
        - name: sequenceId
          in: query
          required: false
          schema:
            type: string
          description: Filter to recipients of a specific sequence
        - name: period
          in: query
          required: false
          schema:
            type: string
            enum: ["1h", "24h", "7d", "30d", "90d"]
          description: "Sliding time window. Ignored when start/end are provided."
        - name: start
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Start of custom time range (ISO 8601). Must be used with `end`."
        - name: end
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "End of custom time range (ISO 8601). Must be used with `start`. Max range: 90 days."
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Recipients per page (max 100)
        - name: includeMachineEngagement
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: Include detected scanner, preview, and tracked asset open/click events in recipient engagement arrays.
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  recipients:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                          nullable: true
                        opened:
                          type: array
                          items:
                            type: object
                            properties:
                              campaignId:
                                type: string
                                nullable: true
                              subject:
                                type: string
                                nullable: true
                              machine:
                                type: boolean
                              engagementQuality:
                                type: string
                                enum: [human, machine, asset]
                              classificationReasons:
                                type: array
                                items:
                                  type: string
                              at:
                                type: string
                                format: date-time
                        clicked:
                          type: array
                          items:
                            type: object
                            properties:
                              campaignId:
                                type: string
                                nullable: true
                              subject:
                                type: string
                                nullable: true
                              url:
                                type: string
                                nullable: true
                              machine:
                                type: boolean
                              engagementQuality:
                                type: string
                                enum: [human, machine, asset]
                              classificationReasons:
                                type: array
                                items:
                                  type: string
                              at:
                                type: string
                                format: date-time
                        unsubscribed:
                          type: boolean
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Campaign or sequence not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /webhooks:
    get:
      operationId: listOutboundWebhooks
      summary: List outbound webhooks
      description: Lists customer-configured outbound webhook endpoints for the authenticated company.
      tags:
        - Webhooks
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhooks:
                    type: array
                    items:
                      $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      operationId: createOutboundWebhook
      summary: Create outbound webhook
      description: Creates an outbound webhook endpoint. The signing secret is returned only in this response.
      tags:
        - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url]
              properties:
                name:
                  type: string
                  example: "Production webhook"
                url:
                  type: string
                  format: uri
                  example: "https://example.com/sequenzy/webhooks"
                events:
                  type: array
                  description: Omit to subscribe to default email and SMS lifecycle events plus subscriber.invalid, subscriber.created, and subscriber.unsubscribed. Add email.opened, email.clicked, email.replied, subscriber.updated, subscriber.list_subscribed, subscriber.list_unsubscribed, sequence.finished, and sequence.failed explicitly for engagement, inbound reply, profile sync, per-list consent sync, or sequence lifecycle events. SMS events (sms.sent, sms.delivered, sms.failed, sms.opted_out) are included in the defaults.
                  items:
                    $ref: "#/components/schemas/OutboundWebhookEventType"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "400":
          description: Invalid URL or event type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}:
    patch:
      operationId: updateOutboundWebhook
      summary: Update outbound webhook
      description: Updates an outbound webhook endpoint URL, name, status, or subscribed events. Changing the URL or enabling the endpoint resets stored endpoint failure state.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                status:
                  type: string
                  enum: [enabled, disabled]
                events:
                  type: array
                  items:
                    $ref: "#/components/schemas/OutboundWebhookEventType"
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "400":
          description: Invalid update
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteOutboundWebhook
      summary: Delete outbound webhook
      description: Permanently deletes an outbound webhook endpoint along with its delivery history. To keep the endpoint but stop deliveries, use PATCH with status "disabled" instead.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /webhooks/{id}/secrets:
    post:
      operationId: addOutboundWebhookSigningSecret
      summary: Add outbound webhook signing secret
      description: Adds another active signing secret. Requests are signed once per active secret in the same signature header.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Secret added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}/secrets/{secretId}:
    delete:
      operationId: removeOutboundWebhookSigningSecret
      summary: Remove outbound webhook signing secret
      description: Removes an active signing secret. A webhook must keep at least one signing secret.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: secretId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Secret removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  webhook:
                    $ref: "#/components/schemas/OutboundWebhookEndpoint"
        "400":
          description: Last signing secret cannot be removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook or signing secret not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /webhooks/{id}/test:
    post:
      operationId: testOutboundWebhook
      summary: Send test outbound webhook
      description: >-
        Queues a test delivery for a webhook endpoint and resets stored endpoint
        failure state. The test is delivered to this endpoint even when the
        endpoint subscribes to no event types, and the queued delivery is
        returned so you can poll its status without waiting for it to appear in
        the delivery list.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Test delivery queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  eventId:
                    type: string
                  queuedDeliveries:
                    type: integer
                  delivery:
                    $ref: "#/components/schemas/OutboundWebhookDelivery"
        "400":
          description: Webhook is disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /webhooks/{id}/deliveries:
    get:
      operationId: listOutboundWebhookDeliveries
      summary: List outbound webhook deliveries
      description: Lists recent delivery attempts for an outbound webhook endpoint.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deliveries:
                    type: array
                    items:
                      $ref: "#/components/schemas/OutboundWebhookDelivery"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /webhooks/{id}/deliveries/{deliveryId}/attempts:
    get:
      operationId: listOutboundWebhookDeliveryAttempts
      summary: List outbound webhook delivery attempts
      description: Lists the latest HTTP attempt summary for a webhook delivery.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: deliveryId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  attempts:
                    type: array
                    items:
                      $ref: "#/components/schemas/OutboundWebhookDeliveryAttempt"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook or delivery not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /webhooks/{id}/deliveries/{deliveryId}/replay:
    post:
      operationId: replayOutboundWebhookDelivery
      summary: Replay outbound webhook delivery
      description: Queues a webhook delivery for another signed POST attempt and resets stored endpoint failure state.
      tags:
        - Webhooks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: deliveryId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Replay queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  delivery:
                    $ref: "#/components/schemas/OutboundWebhookDelivery"
        "400":
          description: Webhook is disabled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Webhook or delivery not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ============================================================================
  # Team Operations
  # ============================================================================
  /team:
    get:
      operationId: listTeamMembers
      summary: List team members
      description: Lists the company owner, team members, and pending or expired invitations.
      tags:
        - Team
      responses:
        "200":
          description: Team members returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  members:
                    type: array
                    items:
                      $ref: "#/components/schemas/TeamMember"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: No access to this team
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /team/invitations:
    post:
      operationId: inviteTeamMember
      summary: Invite a team member
      description: Adds an existing Sequenzy user to the team directly, or emails an invitation to a new user. Requires owner or admin access.
      tags:
        - Team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                email:
                  type: string
                  format: email
                  minLength: 1
                  description: Email address to invite.
                role:
                  type: string
                  enum: [admin, viewer, restricted]
                  description: Role for the new member. Restricted members can open direct campaign links only.
                canManageBilling:
                  type: boolean
                  default: false
                  description: Whether the member can manage billing. Only the company owner can grant this.
              required:
                - email
                - role
      responses:
        "200":
          description: Member added or invitation sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  member:
                    $ref: "#/components/schemas/TeamMember"
                  invitation:
                    type: object
                    properties:
                      id:
                        type: string
                      email:
                        type: string
                      role:
                        type: string
                        enum: [admin, viewer, restricted]
                      canManageBilling:
                        type: boolean
                      status:
                        type: string
                        example: "pending"
                      invitedAt:
                        type: string
                        format: date-time
                      expiresAt:
                        type: string
                        format: date-time
        "400":
          description: Invalid email or owner email
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Admin access or owner billing grant required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Company or inviter not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Already a member or invitation already pending
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Invitation email could not be sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /team/invitations/{invitationId}:
    delete:
      operationId: cancelTeamInvitation
      summary: Cancel a team invitation
      description: Cancels a pending or expired team invitation. Requires owner or admin access.
      tags:
        - Team
      parameters:
        - name: invitationId
          in: path
          required: true
          schema:
            type: string
          description: Invitation ID.
      responses:
        "200":
          description: Invitation cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        "400":
          description: Invitation already accepted or cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Admin access required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  # ============================================================================
  # Conversation (Inbox) Operations
  # ============================================================================
  /conversations:
    get:
      operationId: listConversations
      summary: List conversations
      description: Lists inbox conversations with subscriber replies, filtered by status, unread flag, or search term.
      tags:
        - Conversations
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [all, open, closed]
            default: all
          description: Filter by conversation status.
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search in subject, subscriber email, or subscriber name.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number.
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Results per page.
        - name: unread
          in: query
          required: false
          schema:
            type: string
            enum: ["true"]
          description: Pass "true" to only return conversations with unread messages.
      responses:
        "200":
          description: Conversations returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  conversations:
                    type: array
                    items:
                      $ref: "#/components/schemas/ConversationSummary"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "400":
          description: Invalid status filter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /conversations/{conversationId}:
    get:
      operationId: getConversation
      summary: Get conversation
      description: Returns one conversation with all messages, originating campaign or sequence context, and subscriber details.
      tags:
        - Conversations
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
          description: Conversation ID.
      responses:
        "200":
          description: Conversation returned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  conversation:
                    type: object
                    description: Conversation with messages, context, and subscriber.
                    additionalProperties: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /conversations/{conversationId}/messages:
    post:
      operationId: sendConversationMessage
      summary: Send conversation message
      description: Sends an email reply to the subscriber or adds an internal note. Replies reopen closed conversations.
      tags:
        - Conversations
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
          description: Conversation ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                type:
                  type: string
                  enum: [outbound, note]
                  default: outbound
                  description: outbound sends an email reply, note adds an internal team note.
                subject:
                  type: string
                  description: Message subject. Defaults to the conversation subject.
                bodyText:
                  type: string
                  description: Plain text body. Outbound messages require bodyText or bodyHtml.
                bodyHtml:
                  type: string
                  description: HTML body. Outbound messages require bodyText or bodyHtml.
      responses:
        "200":
          description: Message created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    $ref: "#/components/schemas/ConversationMessage"
        "400":
          description: Message body is required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Message could not be created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /conversations/{conversationId}/status:
    post:
      operationId: updateConversationStatus
      summary: Update conversation status
      description: Opens or closes a conversation.
      tags:
        - Conversations
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
          description: Conversation ID.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                status:
                  type: string
                  enum: [open, closed]
                  description: New conversation status.
              required:
                - status
      responses:
        "200":
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  conversation:
                    type: object
                    properties:
                      id:
                        type: string
                      status:
                        type: string
                        enum: [open, closed]
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Conversation could not be updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /conversations/{conversationId}/read:
    post:
      operationId: markConversationRead
      summary: Mark conversation read
      description: Marks all unread inbound messages in a conversation as read and clears the unread flag.
      tags:
        - Conversations
      parameters:
        - name: conversationId
          in: path
          required: true
          schema:
            type: string
          description: Conversation ID.
      responses:
        "200":
          description: Conversation marked read
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  updated:
                    type: integer
                    description: Number of messages marked read.
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Conversation not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /products:
    get:
      summary: List products
      description: Lists products in the catalog. Includes products synced from Stripe, Shopify/WooCommerce, and products pushed via the Commerce API.
      operationId: listProducts
      tags:
        - Products
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: Maximum number of products to return
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Number of products to skip
        - name: provider
          in: query
          schema:
            type: string
            enum: [api, stripe, shopify, woocommerce, manual]
          description: Filter products by source provider
        - name: search
          in: query
          schema:
            type: string
          description: Filter products by title
      responses:
        "200":
          description: Products listed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  products:
                    type: array
                    items:
                      $ref: "#/components/schemas/CommerceProduct"
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      count:
                        type: integer
                        description: Number of products in this page
                      total:
                        type: integer
                        description: Total products matching the filters across the whole catalog
                      hasMore:
                        type: boolean
                        description: Whether more products remain. Page until this is false.
        "400":
          description: Invalid provider filter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    post:
      summary: Upsert products
      description: Creates or updates up to 100 products, keyed by your productId. Products pushed here behave like Shopify/WooCommerce products - they power product blocks, replenishment reminders, and back-in-stock notifications. Stock transitions trigger back-in-stock events for waiting subscribers. Updates are partial - omitted optional fields keep their stored values; pass an explicit null to clear one.
      operationId: upsertProducts
      tags:
        - Products
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - products
              properties:
                products:
                  type: array
                  minItems: 1
                  maxItems: 100
                  description: Products to create or update
                  items:
                    type: object
                    required:
                      - productId
                      - title
                    properties:
                      productId:
                        type: string
                        description: Your product identifier. Used as the upsert key.
                        example: "SKU-PROTEIN-1KG"
                      title:
                        type: string
                        example: "Protein Powder"
                      description:
                        type: string
                        nullable: true
                        example: "Whey protein, 1kg bag"
                      imageUrl:
                        type: string
                        nullable: true
                        example: "https://cdn.example.com/protein.jpg"
                      url:
                        type: string
                        nullable: true
                        description: Public product page URL
                        example: "https://store.example.com/products/protein"
                      priceCents:
                        type: integer
                        nullable: true
                        description: Price in cents. Defaults to the lowest variant price when variants are provided.
                        example: 8850
                      compareAtPriceCents:
                        type: integer
                        nullable: true
                        example: 9900
                      currency:
                        type: string
                        nullable: true
                        description: ISO 4217 currency code
                        example: "USD"
                      inStock:
                        type: boolean
                        description: Defaults to true, or to whether any variant is available when variants are provided.
                      providerCreatedAt:
                        type: string
                        format: date-time
                        nullable: true
                        description: Product creation time in the source catalog. Omit to preserve the stored value; pass null to clear it.
                      variants:
                        type: array
                        description: Product variants. When provided, the variant list is replaced entirely (an empty array removes all variants). Omit to leave existing variants unchanged.
                        items:
                          type: object
                          required:
                            - variantId
                            - title
                          properties:
                            variantId:
                              type: string
                              description: Your variant identifier
                              example: "SKU-PROTEIN-1KG-VANILLA"
                            title:
                              type: string
                              example: "Vanilla"
                            sku:
                              type: string
                              nullable: true
                            priceCents:
                              type: integer
                              nullable: true
                            compareAtPriceCents:
                              type: integer
                              nullable: true
                            imageUrl:
                              type: string
                              nullable: true
                            inStock:
                              type: boolean
                              description: Defaults to inventoryQuantity > 0 when provided, otherwise true.
                            inventoryQuantity:
                              type: integer
                              nullable: true
                            options:
                              type: array
                              items:
                                type: object
                                required:
                                  - name
                                  - value
                                properties:
                                  name:
                                    type: string
                                    example: "Flavor"
                                  value:
                                    type: string
                                    example: "Vanilla"
      responses:
        "200":
          description: Products upserted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  upserted:
                    type: integer
                    example: 1
                  backInStockEventsTriggered:
                    type: integer
                    description: Number of back-in-stock notifications triggered by stock transitions in this upsert
                    example: 0
                  products:
                    type: array
                    items:
                      $ref: "#/components/schemas/CommerceProduct"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /products/{productId}:
    get:
      summary: Get product
      description: Returns a product previously pushed via the Commerce API, identified by your productId.
      operationId: getProduct
      tags:
        - Products
      parameters:
        - name: productId
          in: path
          required: true
          schema:
            type: string
          description: Your product identifier
      responses:
        "200":
          description: Product found
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  product:
                    $ref: "#/components/schemas/CommerceProduct"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
    delete:
      summary: Delete product
      description: Deletes a product previously pushed via the Commerce API, identified by your productId. Products synced from other providers are not affected.
      operationId: deleteProduct
      tags:
        - Products
      parameters:
        - name: productId
          in: path
          required: true
          schema:
            type: string
          description: Your product identifier
      responses:
        "200":
          description: Product deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  deleted:
                    type: boolean
                    example: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Product not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

        "503":
          $ref: "#/components/responses/TransientDatabaseUnavailable"
  /orders:
    post:
      summary: Push order
      description: Pushes a normalized order from any e-commerce platform. Triggers the matching ecommerce.* event (order placed, cancelled, fulfilled, or refunded), updates the customer's revenue attributes (ltv, totalSpent, ordersCount, aov), cancels superseded commerce automations, and schedules replenishment reminders. Processing is asynchronous.
      operationId: pushOrder
      tags:
        - Orders
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - orderId
                - totalCents
                - currency
                - customer
              properties:
                orderId:
                  type: string
                  description: Unique order identifier in your platform. Used for idempotency - pushing the same orderId twice never double counts revenue.
                  example: "order-1001"
                orderNumber:
                  type: string
                  nullable: true
                  description: Human-facing order number, if different from orderId
                  example: "1001"
                status:
                  type: string
                  enum: [placed, cancelled, fulfilled, refunded]
                  default: placed
                  description: Lifecycle status of this order event
                totalCents:
                  type: integer
                  minimum: 0
                  description: Order total in cents
                  example: 8850
                currency:
                  type: string
                  description: ISO 4217 currency code
                  example: "USD"
                orderedAt:
                  type: string
                  format: date-time
                  description: ISO 8601 timestamp of when the order happened. Defaults to now.
                  example: "2026-06-01T12:00:00.000Z"
                customer:
                  $ref: "#/components/schemas/CommerceCustomer"
                items:
                  type: array
                  description: Order line items
                  items:
                    $ref: "#/components/schemas/CommerceOrderItem"
                refundAmountCents:
                  type: integer
                  nullable: true
                  description: For refunded orders - refunded amount in cents
                customerTotals:
                  type: object
                  nullable: true
                  description: Authoritative customer aggregates from your platform. When provided, these override Sequenzy's additive revenue bookkeeping.
                  properties:
                    ordersCount:
                      type: integer
                      minimum: 0
                      example: 5
                    totalSpentCents:
                      type: integer
                      minimum: 0
                      example: 50000
                properties:
                  type: object
                  additionalProperties: true
                  description: Extra event properties to attach to the triggered ecommerce.* event
      responses:
        "202":
          description: Order queued for processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  queued:
                    type: boolean
                    example: true
                  jobId:
                    type: string
                  orderId:
                    type: string
                    example: "order-1001"
                  status:
                    type: string
                    example: "placed"
        "400":
          description: Validation error (e.g. invalid orderedAt)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Failed to queue order
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /checkouts:
    post:
      summary: Track checkout started
      description: Tracks a started checkout and triggers the ecommerce.checkout_started event, which can power abandoned checkout automations.
      operationId: trackCheckoutStarted
      tags:
        - Orders
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - checkoutId
                - customer
              properties:
                checkoutId:
                  type: string
                  description: Unique checkout identifier in your platform
                  example: "checkout-abc123"
                totalCents:
                  type: integer
                  nullable: true
                  minimum: 0
                  description: Checkout total in cents
                  example: 8850
                currency:
                  type: string
                  nullable: true
                  description: ISO 4217 currency code
                  example: "USD"
                checkoutUrl:
                  type: string
                  nullable: true
                  description: URL the customer can use to resume the checkout
                  example: "https://store.example.com/checkout/abc123"
                customer:
                  $ref: "#/components/schemas/CommerceCustomer"
                items:
                  type: array
                  description: Checkout line items
                  items:
                    $ref: "#/components/schemas/CommerceOrderItem"
                properties:
                  type: object
                  additionalProperties: true
                  description: Extra event properties to attach to the triggered event
      responses:
        "200":
          description: Checkout tracked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  checkoutId:
                    type: string
                    example: "checkout-abc123"
                  subscriberId:
                    type: string
                  eventId:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /back-in-stock:
    post:
      summary: Register back-in-stock request
      description: Registers a customer's request to be notified when a product (pushed via the Commerce API) is back in stock. When a later product upsert marks the product or variant in stock again, the ecommerce.back_in_stock event fires for waiting subscribers.
      operationId: registerBackInStock
      tags:
        - Products
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - productId
                - customer
              properties:
                productId:
                  type: string
                  description: Your product identifier
                  example: "SKU-PROTEIN-1KG"
                variantId:
                  type: string
                  description: Your variant identifier. Defaults to productId for products without variants.
                  example: "SKU-PROTEIN-1KG-VANILLA"
                productTitle:
                  type: string
                  description: Product title snapshot. Defaults to the synced product title.
                variantTitle:
                  type: string
                  description: Variant title snapshot. Defaults to the synced variant title.
                customer:
                  $ref: "#/components/schemas/CommerceCustomer"
      responses:
        "200":
          description: Back-in-stock request registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  subscriberId:
                    type: string
        "400":
          description: Failed to create subscriber
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /web-tracking-identities:
    post:
      operationId: mintWebTrackingIdentity
      summary: Mint a web tracking identity token
      description: Mints a short-lived HMAC proof bound to one active web tracking key, workspace, and normalized email. Identify the key by keyId or by its publishable publicKey value. If the email is not yet a contact, one is created (active, no lists, no automations triggered) so identified events are attributed instead of being silently dropped. Call this only from an authenticated backend; never expose the secret API key in browser code. Identified browser events without this proof are rejected before queueing. Requires commerce:write, automations:trigger, and subscribers:write (minting can create the contact).
      tags:
        - Web Tracking Keys
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                keyId:
                  type: string
                  description: Internal web tracking key ID. Provide this or publicKey.
                publicKey:
                  type: string
                  description: Publishable key value (seq_pk_...). Provide this or keyId.
                email:
                  type: string
                  format: email
                ttlHours:
                  type: number
                  minimum: 0.016666666666666666
                  maximum: 720
                  default: 24
      responses:
        "200":
          description: Identity proof minted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  keyId:
                    type: string
                  publicKey:
                    type: string
                  email:
                    type: string
                    format: email
                  subscriberId:
                    type: string
                    description: Contact the minted identity resolves to; created on first mint for a new email.
                  identityToken:
                    type: string
                  expiresAt:
                    type: string
                    format: date-time
        "400":
          description: Invalid email or token lifetime, neither keyId nor publicKey provided, or the email cannot be added as a contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing commerce:write, automations:trigger, or subscribers:write
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Active web tracking key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /web-tracking-keys:
    get:
      operationId: listWebTrackingKeys
      summary: List web tracking keys
      description: Lists the publishable keys that let a website send on-site events (product views, cart activity, collection views, search) into this workspace. Each key includes a paste-ready install snippet and its origin allowlist. A key whose lastUsedAt is null has not successfully authenticated an event yet; it may be undeployed, have no instrumented traffic, or be sending requests rejected by its origin allowlist. Shopify stores use the storefront pixel instead. Requires the integrations:manage scope.
      tags:
        - Web Tracking Keys
      responses:
        "200":
          description: Web tracking keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebTrackingKey"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the integrations:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: createWebTrackingKey
      summary: Create a web tracking key
      description: Creates a publishable key for the browser tracking SDK and returns the script tag to install. The key ships in page source by design and authorizes storefront events only, never the rest of the API. Events start flowing once the snippet is deployed and nothing is backfilled for the period before that. Always pass allowedOrigins - an empty allowlist accepts events from any site. Requires the integrations:manage scope.
      tags:
        - Web Tracking Keys
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Human-readable label, e.g. Storefront.
                allowedOrigins:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                  description: "Origins allowed to use this key. A bare domain is read as https. A leading *. matches subdomains at any depth but not the apex. Omitting this leaves the key unrestricted."
      responses:
        "200":
          description: Key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  key:
                    $ref: "#/components/schemas/WebTrackingKey"
                  message:
                    type: string
        "400":
          description: Invalid name, or an allowedOrigins entry that is not a valid origin. The whole request is rejected rather than saving the valid subset.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the integrations:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /web-tracking-keys/{id}:
    get:
      operationId: getWebTrackingKey
      summary: Get a web tracking key
      description: Returns one web tracking key with its install snippet and ingest endpoint. The snippet embeds both the publishable key and the workspace id, so use it as returned rather than rebuilding it. Requires the integrations:manage scope.
      tags:
        - Web Tracking Keys
      parameters:
        - name: id
          in: path
          required: true
          description: Web tracking key ID.
          schema:
            type: string
      responses:
        "200":
          description: Web tracking key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  key:
                    $ref: "#/components/schemas/WebTrackingKey"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the integrations:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Web tracking key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: updateWebTrackingKey
      summary: Update a web tracking key
      description: Renames a key, replaces its allowed origins, or revokes it. allowedOrigins replaces the whole list rather than appending. Revoking stops events within about a minute while preserving the key value, so the matching snippet can still be found and removed from the site. Requires the integrations:manage scope.
      tags:
        - Web Tracking Keys
      parameters:
        - name: id
          in: path
          required: true
          description: Web tracking key ID.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  maxLength: 255
                allowedOrigins:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                  description: Replacement allowlist. Pass an empty array to make the key unrestricted.
                isActive:
                  type: boolean
                  description: Set false to revoke the key, true to re-enable a revoked one.
      responses:
        "200":
          description: Key updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  key:
                    $ref: "#/components/schemas/WebTrackingKey"
        "400":
          description: An allowedOrigins entry is not a valid origin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the integrations:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Web tracking key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: deleteWebTrackingKey
      summary: Delete a web tracking key
      description: Permanently deletes a web tracking key. Cached authorization expires within one minute, after which requests from a remaining snippet are rejected. Remove the snippet as well. Prefer revoking with isActive false when the key may be needed again. Requires the integrations:manage scope.
      tags:
        - Web Tracking Keys
      parameters:
        - name: id
          in: path
          required: true
          description: Web tracking key ID.
          schema:
            type: string
      responses:
        "200":
          description: Key deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: API key is missing the integrations:manage scope
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Web tracking key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

tags:
  - name: Subscribers
    description: Manage subscribers, tags, and events
  - name: Events
    description: Published payload schemas for built-in events
  - name: Analytics
    description: Aggregated email metrics and recipient engagement
  - name: Transactional
    description: Send and manage transactional emails
  - name: Widgets
    description: Embeddable widgets for your website
  - name: Webhooks
    description: Configure outbound lifecycle event webhooks
  - name: Products
    description: Product catalog for any e-commerce platform via the Commerce API
  - name: Orders
    description: Push orders and checkouts from any e-commerce platform via the Commerce API
  - name: Email Blocks
    description: Field reference for the email block types accepted by every blocks array
  - name: Email Components
    description: Reusable email components and the per-company default email footer
  - name: Web Tracking Keys
    description: Publishable keys for the browser tracking SDK that sends on-site events from any website
