openapi: 3.0.3
info:
  title: NoChurn API
  version: v1
  description: |
    Recover failed payments, prevent cancellations and win back churned customers.
    NoChurn runs in the EU: all data is hosted in the EU.

    Authentication: `Authorization: Bearer nc_live_…` (or `nc_test_…` for sandbox).
    The dashboard uses a session cookie instead (see the Auth endpoints).

    Errors always use the Error schema: `{ error: { code, message, fix, docs_url } }`.
    Customers can be referenced by their NoChurn id (`cst_…`), their Stripe id (`cus_…`)
    or your own id (`external_id`), see GET /v1/customers/resolve.

    NoChurn's own client code (the widget, the MCP server, the SDKs) sends `NoChurn-Client: <name>/<semver>`
    (e.g. `widget/1.0.0`, `mcp/0.1.0`, `node/1.2.0`; name `^[a-z][a-z0-9-]{1,31}$`, strict semver, 32 characters
    at most) so that GET /v1/account/clients can tell you when to update it. Optional: your own code sends nothing.
servers:
  - url: https://api.nochurn.sh
    description: Production
tags:
  - name: Customers
    description: "The people and companies who pay you: their three ids (cst_, cus_, your external_id), churn score, access and custom attributes."
  - name: Billing
    description: Subscriptions and payments sent by your own billing, when you don't use a connected billing tool.
  - name: Subscriptions
    description: Subscriptions of a customer, from the billing source.
  - name: Invoices
    description: Invoices of a customer, from the billing source.
  - name: Payment methods
    description: "Payment methods of a customer: brand, last4, expiry and type only, never card numbers."
  - name: Events
    description: Every decision NoChurn takes, with its reason (nevt_…).
  - name: Flows
    description: "Recovery, prevention, cancel and win-back flows: drafts, publishing, emails and runs."
  - name: Cancel flow
    description: "The cancel flow: its configuration, design, sessions and the customer's answers."
  - name: Messages
    description: "Emails NoChurn sends: the review queue, customers' replies and addresses never emailed again."
  - name: Chargebacks
    description: "Disputes and early fraud warnings: evidence, replies and refunds."
  - name: Data & AI
    description: Custom data, churn drivers, campaigns, queries and feedback.
  - name: Integrations
    description: Billing tools, payment providers, app stores and help desks connected to NoChurn.
  - name: Stripe
    description: "Stripe Connect: the connection, the import and its settings."
  - name: Email domain
    description: "Your sending domain: sender, DNS records and verification."
  - name: Developers
    description: Webhook endpoints, their deliveries, and the versions of NoChurn's client code you run.
  - name: API keys
    description: Secret keys (nc_…) and publishable keys (pk_…), full or restricted.
  - name: Logs
    description: "The unified log: API requests, webhooks and decisions."
  - name: Account
    description: Account settings, setup progress and sending schedule.
  - name: Team
    description: Members, roles and invitations.
  - name: AI apps
    description: AI apps connected over MCP (OAuth), and live requests waiting for a human.
  - name: Agent integration
    description: "What coding agents use to add NoChurn to an app: platform detection, tested snippets and the checklist."
  - name: Sandbox
    description: "Test-mode scenarios: create a customer, fail a payment, remove a card, run a cancel flow."
  - name: Widget
    description: Calls made by the browser script (js.nochurn.sh) with a publishable key.
  - name: In-app
    description: The in-app banner and wall of one customer, for the browser script.
security:
  - bearer: []
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: Secret API key (nc_live_… or nc_test_…), or an AI app's OAuth access token (ncat_…, test mode by default, docs/MCP.md)
  schemas:
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - fix
            - docs_url
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - not_found
                - invalid_request
                - conflict
                - internal_error
                - rate_limited
                - customer_not_found
                - ambiguous_customer
                - stripe_not_configured
                - stripe_error
                - two_factor_required
                - attribute_type_mismatch
                - attribute_limit_reached
                - provider_not_available
                - capability_missing
                - dispute_already_submitted
                - dispute_not_supported_by_provider
                - provider_error
                - evidence_file_unsupported
                - evidence_file_too_large
                - evidence_files_limit
                - logo_unsupported
                - logo_too_large
                - plan_billing_not_configured
                - custom_plan_required
                - test_mode_only
                - live_approval_required
                - live_read_not_granted
                - insufficient_scope
                - publishable_key_required
                - payload_too_large
                - subscription_not_found
                - subscription_required
                - email_domain_provisioning_failed
                - insufficient_data
                - nothing_to_resume
            message:
              type: string
            fix:
              type: string
            docs_url:
              type: string
              format: uri
            retry_after:
              type: integer
              description: Seconds to wait (rate_limited only).
          nullable: false
      nullable: false
    Account:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - name
        - billing_source
        - sandbox
        - default_locale
        - plan
        - external_id_metadata_key
        - require_two_factor
        - timezone
        - audience
        - reporting_currency
        - onboarding
        - disputes_settings
        - created_at
        - updated_at
      properties:
        object:
          type: string
          enum:
            - account
        id:
          type: string
          example: acc_7Zf3kQ2mV9xYpL4a
        name:
          type: string
        billing_source:
          type: string
          enum:
            - stripe
            - api
        sandbox:
          type: boolean
        default_locale:
          type: string
          enum:
            - en
            - de
            - fr
            - it
            - pt
        plan:
          type: string
          enum:
            - free
            - paid
            - custom
          description: "NoChurn's own plan: details in GET /v1/billing/plan."
        external_id_metadata_key:
          type: string
          description: "Stripe customer metadata key copied into external_id. Default: user_id."
        require_two_factor:
          type: boolean
          description: Every member must use two-factor authentication. Owners only, from a 2FA session.
        timezone:
          type: string
          example: Europe/Paris
          description: 'IANA time zone: "this month" and "today" in the dashboard.'
        audience:
          type: string
          nullable: true
          enum:
            - b2b
            - b2c
            - null
          description: "Who your customers are: sets the default send window (GET /v1/account/send_schedule)."
        reporting_currency:
          type: string
          nullable: true
          example: eur
          description: "Currency the dashboard converts every amount into (ECB rates). null: the connected Stripe account's default currency, else the one most customers pay in."
        onboarding:
          type: object
          additionalProperties: false
          required:
            - step
            - skipped
            - completed_at
          properties:
            step:
              type: string
              enum:
                - billing
                - support
                - cancel_modal
                - email_domain
                - done
              description: Where the onboarding resumes.
            skipped:
              type: array
              items:
                type: string
                enum:
                  - support
                  - cancel_modal
                  - email_domain
              description: Set up later.
            completed_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
        disputes_settings:
          type: object
          additionalProperties: false
          required:
            - auto_submit
            - auto_submit_hours_before
            - auto_refund_fraud_warnings
            - auto_refund_max_cents
            - cancel_subscription_on_fraud_refund
            - notify_email
            - notify_slack
            - contest_cost_cents
            - contest_cost_currency
            - auto_submit_min_score
          properties:
            auto_submit:
              type: boolean
              description: "Off by default: a human approves and sends."
            auto_submit_hours_before:
              type: integer
              minimum: 1
              maximum: 168
            auto_refund_fraud_warnings:
              type: boolean
              description: Off by default.
            auto_refund_max_cents:
              type: integer
              nullable: true
            cancel_subscription_on_fraud_refund:
              type: boolean
            notify_email:
              type: boolean
            notify_slack:
              type: boolean
              description: Post to the Slack channel of Integrations (if connected).
            contest_cost_cents:
              type: integer
              nullable: true
              description: "What contesting costs you (provider fee and time). Unset: no suggestion to accept."
            contest_cost_currency:
              type: string
              nullable: true
              description: Currency of contest_cost_cents; null = the reporting currency.
            auto_submit_min_score:
              type: integer
              minimum: 0
              maximum: 100
              description: Automatic submission only from this chance of winning (70 by default).
          nullable: false
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      nullable: false
    ApiKey:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - name
        - kind
        - mode
        - access
        - permissions
        - last4
        - allowed_origins
        - token
        - first_used_at
        - last_used_at
        - revoked_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - api_key
        id:
          type: string
          example: key_3Srt9txFqHsFTSjb
        name:
          type: string
        kind:
          type: string
          enum:
            - secret
            - publishable
        mode:
          type: string
          enum:
            - live
            - test
        access:
          type: string
          enum:
            - full
            - restricted
          description: "full: every resource. restricted: see permissions."
        permissions:
          type: object
          nullable: true
          additionalProperties: false
          description: Level per resource (none, read, write). null for publishable keys.
          properties:
            account:
              type: string
              enum:
                - none
                - read
                - write
            api_keys:
              type: string
              enum:
                - none
                - read
                - write
            team:
              type: string
              enum:
                - none
                - read
                - write
            events:
              type: string
              enum:
                - none
                - read
                - write
            customers:
              type: string
              enum:
                - none
                - read
                - write
            billing:
              type: string
              enum:
                - none
                - read
                - write
            stripe_connections:
              type: string
              enum:
                - none
                - read
                - write
            logs:
              type: string
              enum:
                - none
                - read
                - write
            flows:
              type: string
              enum:
                - none
                - read
                - write
            email_domains:
              type: string
              enum:
                - none
                - read
                - write
            messages:
              type: string
              enum:
                - none
                - read
                - write
            webhooks:
              type: string
              enum:
                - none
                - read
                - write
            cancel_sessions:
              type: string
              enum:
                - none
                - read
                - write
            reviews:
              type: string
              enum:
                - none
                - read
                - write
            dashboard:
              type: string
              enum:
                - none
                - read
                - write
            insights:
              type: string
              enum:
                - none
                - read
                - write
            integrations:
              type: string
              enum:
                - none
                - read
                - write
            queries:
              type: string
              enum:
                - none
                - read
                - write
            disputes:
              type: string
              enum:
                - none
                - read
                - write
            sandbox:
              type: string
              enum:
                - none
                - read
                - write
        last4:
          type: string
        allowed_origins:
          type: array
          items:
            type: string
          nullable: true
          description: "Publishable keys: origins allowed to call the public endpoints (null on secret keys)."
        token:
          type: string
          nullable: true
          description: "Secret keys: only on creation, store it now. Publishable keys: always."
        first_used_at:
          type: string
          format: date-time
          nullable: true
          description: "First call made with this key (onboarding: waiting for your first event)."
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    Event:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - type
        - subject
        - reason
        - actor
        - livemode
        - data
        - created_at
      properties:
        object:
          type: string
          enum:
            - event
        id:
          type: string
          example: nevt_tZCYqtEDVAvwZjD5
        type:
          type: string
          example: api_key.created
        subject:
          type: object
          additionalProperties: false
          required:
            - type
            - id
          properties:
            type:
              type: string
            id:
              type: string
          nullable: true
        reason:
          type: string
          nullable: true
        actor:
          type: object
          additionalProperties: false
          required:
            - type
            - id
            - name
          properties:
            type:
              type: string
              enum:
                - api_key
                - user
                - system
                - ai
                - ai_agent
                - customer
            id:
              type: string
              nullable: true
              description: "ai_agent: the OAuth grant (ogr_…) of the AI app."
            name:
              type: string
              nullable: true
              description: "ai_agent: the MCP client's name (as it registered)."
          nullable: false
        livemode:
          type: boolean
        data:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
      nullable: false
    StripeConnection:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - stripe_account_id
        - livemode
        - status
        - smart_retries_mode
        - internal_id
        - backfill
        - history
        - last_webhook_at
        - last_webhook_type
        - disconnected_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - stripe_connection
        id:
          type: string
          example: scn_8kQ2mV9xYpL4aZf3
        stripe_account_id:
          type: string
          example: acct_1Nv0FGQ9RKHgCVdK
        livemode:
          type: boolean
        status:
          type: string
          enum:
            - active
            - revoked
            - error
        smart_retries_mode:
          type: string
          enum:
            - stripe
            - nochurn
          description: "stripe: keep Stripe Smart Retries. nochurn: NoChurn schedules the retries (turn Smart Retries off in Stripe)."
        internal_id:
          type: object
          additionalProperties: false
          required:
            - field
            - defaults
            - fields
          properties:
            field:
              type: string
              nullable: true
              description: The field this connection names (PATCH internal_id_field); null = defaults.
              example: custom_fields.user_id
            defaults:
              type: array
              items:
                type: string
              example:
                - custom_fields.user_id
                - code
              description: Read in this order when field is null; user_id is the account's external_id_metadata_key.
            fields:
              type: array
              items:
                type: string
              example:
                - code
                - custom_fields.<name>
              description: The shapes a field may take for this tool (<name> = one key).
          nullable: true
          description: "Where NoChurn reads your app's id for a customer (your user id, stored as external_id) in this tool; null for a tool that carries none (stores, universal webhook). Nothing found: the email decides."
        backfill:
          type: object
          additionalProperties: false
          required:
            - status
            - progress
            - error
            - started_at
            - finished_at
          properties:
            status:
              type: string
              enum:
                - pending
                - running
                - done
                - failed
            progress:
              type: object
              additionalProperties:
                type: integer
              description: "Records imported so far: customers, subscriptions, invoices, payment_methods."
            error:
              type: object
              additionalProperties: false
              required:
                - code
                - message
                - fix
                - docs_url
              properties:
                code:
                  type: string
                  enum:
                    - import_access_denied
                    - import_interrupted
                    - import_failed
                message:
                  type: string
                fix:
                  type: string
                docs_url:
                  type: string
              nullable: true
              description: Why the import stopped and what to do; null unless it failed.
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
        history:
          type: object
          additionalProperties: false
          required:
            - status
            - since
            - progress
            - error
            - started_at
            - finished_at
          properties:
            status:
              type: string
              enum:
                - pending
                - running
                - done
                - failed
            since:
              type: string
              format: date-time
              nullable: true
            progress:
              type: object
              additionalProperties:
                type: integer
              description: "Records imported so far: invoices, charges."
            error:
              type: object
              additionalProperties: false
              required:
                - code
                - message
                - fix
                - docs_url
              properties:
                code:
                  type: string
                  enum:
                    - import_access_denied
                    - import_interrupted
                    - import_failed
                message:
                  type: string
                fix:
                  type: string
                docs_url:
                  type: string
              nullable: true
              description: Why the import stopped and what to do; null unless it failed.
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
          description: 12-month history import (invoices and payment attempts), after the backfill.
        last_webhook_at:
          type: string
          format: date-time
          nullable: true
        last_webhook_type:
          type: string
          nullable: true
          example: invoice.payment_failed
          description: Type of the newest Stripe event received for this connection; null before the first.
        disconnected_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    Customer:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - stripe_customer_id
        - external_id
        - stripe_account
        - livemode
        - email
        - company_domain
        - name
        - locale
        - country
        - region
        - timezone
        - timezone_source
        - attributes
        - churn_score
        - churn_level
        - flag
        - state
        - plan
        - mrr_cents
        - lifetime_value_cents
        - currency
        - payment_method
        - last_seen_at
        - churned_at
        - flows_paused_at
        - signup_at
        - created_at
        - updated_at
        - identifiers
      properties:
        object:
          type: string
          enum:
            - customer
        id:
          type: string
          example: cst_4fJ8sK2pQ9wLm3Xz
          description: NoChurn id, always present.
        stripe_customer_id:
          type: string
          nullable: true
          example: cus_QwErTy123
          description: Stripe customer id, when billed by Stripe.
        external_id:
          type: string
          nullable: true
          example: u_123
          description: The id of this customer in your app.
        stripe_account:
          type: string
          nullable: true
          example: acct_1Nv0FGQ9RKHgCVdK
        livemode:
          type: boolean
        email:
          type: string
          nullable: true
        company_domain:
          type: string
          nullable: true
          example: acme.com
          description: "Domain of the email when it is a company's (not a free, disposable or placeholder address): the customer's logo is GET /v1/logos/{company_domain}."
        name:
          type: string
          nullable: true
        locale:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        region:
          type: string
          nullable: true
          example: TX
          description: State or province of the address.
        timezone:
          type: string
          nullable: true
          example: America/Chicago
          description: IANA time zone used to reach the customer.
        timezone_source:
          type: string
          nullable: true
          enum:
            - api
            - attribute
            - address
            - country_guess
            - ip_country
            - card_country
            - account
            - null
          description: api (sent by you, never overwritten), attribute, address, country_guess (main zone of a multi-zone country), ip_country, card_country, account (fallback).
        attributes:
          type: object
          additionalProperties: true
          description: Custom attributes (M4/M6).
        churn_score:
          type: integer
          nullable: true
        churn_level:
          type: string
          enum:
            - low
            - medium
            - high
            - null
          nullable: true
        flag:
          $ref: "#/components/schemas/CustomerFlag"
        state:
          type: string
          enum:
            - none
            - trialing
            - active
            - in_recovery
            - paused
            - churned
          description: "From billing: in_recovery = an open invoice with a failed attempt; churned = no live subscription."
        plan:
          type: string
          nullable: true
          example: Pro
          description: Plan of the main live subscription.
        mrr_cents:
          type: integer
          description: Monthly recurring revenue, in the minor unit of currency.
        lifetime_value_cents:
          type: integer
          description: Paid invoices seen by NoChurn (12 months of Stripe history, then all).
        currency:
          type: string
          nullable: true
          example: eur
          description: Billing currency. Amounts are never converted.
        payment_method:
          $ref: "#/components/schemas/PaymentMethodSummary"
        last_seen_at:
          type: string
          format: date-time
          nullable: true
          description: From the last_active_at attribute your app sends.
        churned_at:
          type: string
          format: date-time
          nullable: true
        flows_paused_at:
          type: string
          format: date-time
          nullable: true
          description: "Set by Pause all flows: no flow acts on this customer."
        signup_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        identifiers:
          type: array
          items:
            $ref: "#/components/schemas/CustomerIdentifier"
          description: "Every identifier a source gave for this customer, oldest first: how Stripe, your other tools, your app and support all reach this one customer (docs: customer matching)."
        risk:
          type: object
          additionalProperties: false
          required:
            - next_renewal_at
            - cancel_at_period_end
            - score_factors
            - score_computed_at
          properties:
            next_renewal_at:
              type: string
              format: date-time
              nullable: true
              description: Soonest end of the current period of a paying subscription (active or past_due) still to come.
            cancel_at_period_end:
              type: boolean
              nullable: true
              description: That subscription stops then instead of renewing. null without a renewal.
            score_factors:
              type: array
              maxItems: 5
              description: Factors that raised the last churn score, largest first (starting point and lowering factors left out).
              items:
                type: object
                additionalProperties: false
                required:
                  - key
                  - label
                  - points
                properties:
                  key:
                    type: string
                    example: failed_payments
                  label:
                    type: string
                    example: 2 failed payments in 90 days
                  points:
                    type: integer
                nullable: false
            score_computed_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
          description: "With include[]=risk (list only): when the customer renews and why the score is what it is. Early signals (card removed…) are in flag."
      nullable: false
    CustomerIdentifier:
      type: object
      additionalProperties: false
      required:
        - kind
        - provider
        - connection
        - value
        - source
        - created_at
      properties:
        kind:
          type: string
          enum:
            - stripe
            - external
            - email
            - provider
            - nochurn
          description: stripe (cus_), provider (a billing, store or support tool's id), external (your id, current or previous), email, nochurn (the cst_ id of a customer merged into this one, still accepted).
        provider:
          type: string
          nullable: true
          example: chargebee
          description: The tool for stripe and provider ids (stripe, chargebee, app_store, intercom…).
        connection:
          type: string
          nullable: true
          example: bcn_2Kd9sL2mQx7VbN4p
          description: The billing connection (scn_ / bcn_) or support integration (itg_) the id belongs to.
        value:
          type: string
          example: cus_QwErTy123
        source:
          type: string
          enum:
            - api
            - stripe_metadata
            - stripe
            - manual
            - provider
            - support
            - import
            - merge
          description: "Who gave it: api, import, stripe, stripe_metadata, provider, support, manual, merge (moved by a merge)."
        created_at:
          type: string
          format: date-time
      nullable: false
    CustomerDuplicate:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - customer
        - other_customer
        - people
        - identifier
        - reason
        - status
        - suggested_keep
        - kept_customer
        - dismissed_at
        - merged_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - customer_duplicate
        id:
          type: string
          example: cdp_4fJ8sK2pQ9wLm3Xz
        livemode:
          type: boolean
        customer:
          type: string
          description: The pair is stored with the smaller id first.
        other_customer:
          type: string
        people:
          type: array
          minItems: 2
          maxItems: 2
          items:
            $ref: "#/components/schemas/CustomerDuplicatePerson"
          description: Name and email of customer then other_customer; null for one that no longer exists (merged away).
        identifier:
          type: string
          enum:
            - stripe
            - provider
            - external
            - email
          description: "What they share: an email, or ids one source sent together."
        reason:
          type: string
          example: share an email
        status:
          type: string
          enum:
            - open
            - dismissed
            - merged
          description: open (a To do), dismissed (not the same person), merged (one customer absorbed into the other).
        suggested_keep:
          type: string
          nullable: true
          example: cst_4fJ8sK2pQ9wLm3Xz
          description: "Open: the customer to preselect as the one to keep: the one with a live subscription, else the one billed by a tool (Stripe or another), else the oldest. Merged: the kept one. Dismissed: null."
        kept_customer:
          type: string
          nullable: true
          description: "Merged: the customer that holds everything now (the other one no longer exists)."
        dismissed_at:
          type: string
          format: date-time
          nullable: true
        merged_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    CustomerDuplicatePerson:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - email
      properties:
        id:
          type: string
        name:
          type: string
          nullable: true
        email:
          type: string
          nullable: true
      nullable: true
    CustomerDuplicateRun:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - kind
        - status
        - total
        - processed
        - found
        - merged
        - skipped
        - failed
        - failures
        - error
        - started_at
        - finished_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - customer_duplicate_run
        id:
          type: string
          example: cdr_4fJ8sK2pQ9wLm3Xz
        livemode:
          type: boolean
        kind:
          type: string
          enum:
            - scan
            - merge_all
          description: "scan: looks for duplicates among the customers already in base. merge_all: merges every open proposal into its suggested customer."
        status:
          type: string
          enum:
            - queued
            - running
            - done
            - failed
        total:
          type: integer
          description: "scan: customers read. merge_all: open proposals at the start."
        processed:
          type: integer
          description: How many of total were gone through so far (real progress).
        found:
          type: integer
          description: "scan: new proposals created (pairs already proposed, dismissed or merged are never proposed again)."
        merged:
          type: integer
          description: "merge_all: pairs merged into their suggested customer."
        skipped:
          type: integer
          description: "merge_all: pairs decided meanwhile, or folded into another pair by an earlier merge of the run."
        failed:
          type: integer
          description: "merge_all: pairs that could not be merged (see failures); the others went on."
        failures:
          type: array
          description: "merge_all: each failed pair and why (the first 100)."
          items:
            type: object
            additionalProperties: false
            required:
              - duplicate
              - customers
              - reason
            properties:
              duplicate:
                type: string
              customers:
                type: array
                items:
                  type: string
              reason:
                type: string
            nullable: false
        error:
          type: string
          nullable: true
          description: "failed: why the whole run stopped."
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    CustomerDuplicateSummary:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - open_count
        - last_scanned_at
        - scan
        - merge_all
      properties:
        object:
          type: string
          enum:
            - customer_duplicate_summary
        livemode:
          type: boolean
        open_count:
          type: integer
          description: Open proposals of this mode.
        last_scanned_at:
          type: string
          format: date-time
          nullable: true
          description: "End of the last finished scan of this mode (null: never scanned)."
        scan:
          type: object
          additionalProperties: false
          required:
            - object
            - id
            - livemode
            - kind
            - status
            - total
            - processed
            - found
            - merged
            - skipped
            - failed
            - failures
            - error
            - started_at
            - finished_at
            - created_at
          properties:
            object:
              type: string
              enum:
                - customer_duplicate_run
            id:
              type: string
              example: cdr_4fJ8sK2pQ9wLm3Xz
            livemode:
              type: boolean
            kind:
              type: string
              enum:
                - scan
                - merge_all
              description: "scan: looks for duplicates among the customers already in base. merge_all: merges every open proposal into its suggested customer."
            status:
              type: string
              enum:
                - queued
                - running
                - done
                - failed
            total:
              type: integer
              description: "scan: customers read. merge_all: open proposals at the start."
            processed:
              type: integer
              description: How many of total were gone through so far (real progress).
            found:
              type: integer
              description: "scan: new proposals created (pairs already proposed, dismissed or merged are never proposed again)."
            merged:
              type: integer
              description: "merge_all: pairs merged into their suggested customer."
            skipped:
              type: integer
              description: "merge_all: pairs decided meanwhile, or folded into another pair by an earlier merge of the run."
            failed:
              type: integer
              description: "merge_all: pairs that could not be merged (see failures); the others went on."
            failures:
              type: array
              description: "merge_all: each failed pair and why (the first 100)."
              items:
                type: object
                additionalProperties: false
                required:
                  - duplicate
                  - customers
                  - reason
                properties:
                  duplicate:
                    type: string
                  customers:
                    type: array
                    items:
                      type: string
                  reason:
                    type: string
                nullable: false
            error:
              type: string
              nullable: true
              description: "failed: why the whole run stopped."
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
            created_at:
              type: string
              format: date-time
          nullable: true
          description: "The latest scan (null: never)."
        merge_all:
          type: object
          additionalProperties: false
          required:
            - object
            - id
            - livemode
            - kind
            - status
            - total
            - processed
            - found
            - merged
            - skipped
            - failed
            - failures
            - error
            - started_at
            - finished_at
            - created_at
          properties:
            object:
              type: string
              enum:
                - customer_duplicate_run
            id:
              type: string
              example: cdr_4fJ8sK2pQ9wLm3Xz
            livemode:
              type: boolean
            kind:
              type: string
              enum:
                - scan
                - merge_all
              description: "scan: looks for duplicates among the customers already in base. merge_all: merges every open proposal into its suggested customer."
            status:
              type: string
              enum:
                - queued
                - running
                - done
                - failed
            total:
              type: integer
              description: "scan: customers read. merge_all: open proposals at the start."
            processed:
              type: integer
              description: How many of total were gone through so far (real progress).
            found:
              type: integer
              description: "scan: new proposals created (pairs already proposed, dismissed or merged are never proposed again)."
            merged:
              type: integer
              description: "merge_all: pairs merged into their suggested customer."
            skipped:
              type: integer
              description: "merge_all: pairs decided meanwhile, or folded into another pair by an earlier merge of the run."
            failed:
              type: integer
              description: "merge_all: pairs that could not be merged (see failures); the others went on."
            failures:
              type: array
              description: "merge_all: each failed pair and why (the first 100)."
              items:
                type: object
                additionalProperties: false
                required:
                  - duplicate
                  - customers
                  - reason
                properties:
                  duplicate:
                    type: string
                  customers:
                    type: array
                    items:
                      type: string
                  reason:
                    type: string
                nullable: false
            error:
              type: string
              nullable: true
              description: "failed: why the whole run stopped."
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
            created_at:
              type: string
              format: date-time
          nullable: true
          description: "The latest merge all (null: never)."
      nullable: false
    Subscription:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - customer
        - stripe_subscription_id
        - external_id
        - livemode
        - status
        - plan
        - amount_cents
        - currency
        - interval
        - interval_count
        - mrr_cents
        - current_period_end
        - cancel_at_period_end
        - canceled_at
        - paused_until
        - trial_end
        - created_at
      properties:
        object:
          type: string
          enum:
            - subscription
        id:
          type: string
          example: nsub_2mV9xYpL4aZf3kQ8
        customer:
          type: string
        stripe_subscription_id:
          type: string
          nullable: true
        external_id:
          type: string
          nullable: true
        livemode:
          type: boolean
        status:
          type: string
          enum:
            - incomplete
            - incomplete_expired
            - trialing
            - active
            - past_due
            - canceled
            - unpaid
            - paused
        plan:
          type: string
          nullable: true
        amount_cents:
          type: integer
        currency:
          type: string
          nullable: true
        interval:
          type: string
          nullable: true
        interval_count:
          type: integer
        mrr_cents:
          type: integer
        current_period_end:
          type: string
          format: date-time
          nullable: true
        cancel_at_period_end:
          type: boolean
        canceled_at:
          type: string
          format: date-time
          nullable: true
        paused_until:
          type: string
          format: date-time
          nullable: true
        trial_end:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    Invoice:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - customer
        - subscription
        - stripe_invoice_id
        - external_id
        - livemode
        - number
        - status
        - amount_due_cents
        - amount_paid_cents
        - amount_remaining_cents
        - currency
        - attempt_count
        - next_attempt_at
        - failure_code
        - decline_code
        - hosted_invoice_url
        - paid_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - invoice
        id:
          type: string
          example: ninv_9xYpL4aZf3kQ8mV2
        customer:
          type: string
        subscription:
          type: string
          nullable: true
        stripe_invoice_id:
          type: string
          nullable: true
        external_id:
          type: string
          nullable: true
        livemode:
          type: boolean
        number:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - draft
            - open
            - paid
            - uncollectible
            - void
        amount_due_cents:
          type: integer
        amount_paid_cents:
          type: integer
        amount_remaining_cents:
          type: integer
        currency:
          type: string
          nullable: true
        attempt_count:
          type: integer
        next_attempt_at:
          type: string
          format: date-time
          nullable: true
        failure_code:
          type: string
          nullable: true
        decline_code:
          type: string
          nullable: true
        hosted_invoice_url:
          type: string
          nullable: true
        paid_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    PaymentMethod:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - customer
        - stripe_payment_method_id
        - livemode
        - type
        - brand
        - last4
        - exp_month
        - exp_year
        - funding
        - country
        - wallet
        - is_default
        - detached_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - payment_method
        id:
          type: string
          example: npm_L4aZf3kQ8mV29xYp
        customer:
          type: string
          nullable: true
        stripe_payment_method_id:
          type: string
          nullable: true
        livemode:
          type: boolean
        type:
          type: string
          example: card
        brand:
          type: string
          nullable: true
        last4:
          type: string
          nullable: true
        exp_month:
          type: integer
          nullable: true
        exp_year:
          type: integer
          nullable: true
        funding:
          type: string
          nullable: true
          description: credit, debit, prepaid or unknown
        country:
          type: string
          nullable: true
        wallet:
          type: string
          nullable: true
          description: apple_pay, google_pay, link…
        is_default:
          type: boolean
        detached_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
      description: Metadata only. NoChurn never stores card numbers.
    Setup:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - billing_source
        - api_base_url
        - complete
        - steps
        - onboarding
        - counters
        - history_import
      properties:
        object:
          type: string
          enum:
            - setup
        livemode:
          type: boolean
        billing_source:
          type: string
          enum:
            - stripe
            - api
        api_base_url:
          type: string
          description: Public URL of the API (e.g. https://api.nochurn.sh), for the dashboard's copy-paste examples.
        complete:
          type: boolean
          description: Every step done, or skipped (set up later).
        steps:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - done
              - skipped
              - fix
            properties:
              key:
                type: string
                enum:
                  - billing_connected
                  - billing_synced
                  - history_imported
                  - webhook_received
                  - api_key_created
                  - first_api_call
                  - support_connected
                  - cancel_modal_detected
                  - email_domain_verified
                  - cancel_flow_published
                  - recovery_retries_configured
                  - prevention_signals_enabled
              done:
                type: boolean
              skipped:
                type: boolean
              fix:
                type: string
                nullable: true
            nullable: false
        onboarding:
          type: object
          additionalProperties: false
          required:
            - step
            - skipped
            - completed_at
          properties:
            step:
              type: string
              enum:
                - billing
                - support
                - cancel_modal
                - email_domain
                - done
            skipped:
              type: array
              items:
                type: string
            completed_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
        counters:
          type: object
          additionalProperties: false
          required:
            - customers_imported
            - active_customers
            - mrr
            - failed_payments_to_recover
            - high_risk_customers
            - scored_customers
            - first_api_call_at
            - customers_with_multiple_subscriptions
            - sessions_without_subscription_30d
          properties:
            customers_imported:
              type: integer
            active_customers:
              type: integer
            mrr:
              type: array
              description: One entry per currency, largest first. Never converted.
              items:
                type: object
                additionalProperties: false
                required:
                  - currency
                  - cents
                properties:
                  currency:
                    type: string
                  cents:
                    type: integer
                nullable: false
            failed_payments_to_recover:
              type: object
              additionalProperties: false
              required:
                - count
                - amounts
              properties:
                count:
                  type: integer
                amounts:
                  type: array
                  description: One entry per currency, largest first. Never converted.
                  items:
                    type: object
                    additionalProperties: false
                    required:
                      - currency
                      - cents
                    properties:
                      currency:
                        type: string
                      cents:
                        type: integer
                    nullable: false
              nullable: false
              description: Open invoices with at least one failed attempt.
            high_risk_customers:
              type: integer
            scored_customers:
              type: integer
              description: 0 until churn scores are computed (M4).
            first_api_call_at:
              type: string
              format: date-time
              nullable: true
            customers_with_multiple_subscriptions:
              type: integer
              description: Customers with two or more live subscriptions in this mode.
            sessions_without_subscription_30d:
              type: integer
              description: Cancel sessions of the last 30 days opened without a `subscription` (token claim or server request). With customers_with_multiple_subscriptions > 0, sign the token with `subscription` (DECISIONS 72).
          nullable: false
        history_import:
          type: object
          additionalProperties: false
          required:
            - status
            - since
            - progress
            - error
            - started_at
            - finished_at
          properties:
            status:
              type: string
              enum:
                - pending
                - running
                - done
                - failed
            since:
              type: string
              format: date-time
              nullable: true
            progress:
              type: object
              additionalProperties:
                type: integer
            error:
              type: object
              additionalProperties: false
              required:
                - code
                - message
                - fix
                - docs_url
              properties:
                code:
                  type: string
                  enum:
                    - import_access_denied
                    - import_interrupted
                    - import_failed
                message:
                  type: string
                fix:
                  type: string
                docs_url:
                  type: string
              nullable: true
              description: Why the import stopped and what to do; null unless it failed.
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
          nullable: true
          description: 12-month Stripe history import of the latest connection (null with billing by API).
      nullable: false
    StripeAuthorizeUrl:
      type: object
      additionalProperties: false
      required:
        - object
        - url
        - livemode
      properties:
        object:
          type: string
          enum:
            - stripe_authorize_url
        url:
          type: string
          format: uri
        livemode:
          type: boolean
      nullable: false
    Membership:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - role
        - user
        - created_at
      properties:
        object:
          type: string
          enum:
            - membership
        id:
          type: string
          example: mbr_Q2mV9xYpL4aZf3k8
        role:
          type: string
          enum:
            - owner
            - admin
            - member
        user:
          type: object
          additionalProperties: false
          required:
            - id
            - email
            - name
            - two_factor_enabled
            - last_login_at
          properties:
            id:
              type: string
            email:
              type: string
            name:
              type: string
              nullable: true
            two_factor_enabled:
              type: boolean
            last_login_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
        created_at:
          type: string
          format: date-time
      nullable: false
    Invitation:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - email
        - role
        - status
        - url
        - invited_by
        - expires_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - invitation
        id:
          type: string
          example: inv_V9xYpL4aZf3kQ8m2
        email:
          type: string
        role:
          type: string
          enum:
            - owner
            - admin
            - member
        status:
          type: string
          enum:
            - pending
            - accepted
            - revoked
            - expired
        url:
          type: string
          nullable: true
          description: "Invitation link. Only returned at creation: copy it and send it yourself."
        invited_by:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
      nullable: false
    LogEntry:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - occurred_at
        - source
        - type
        - customer
        - summary
        - status
        - error
        - why
        - ref
        - request_id
      properties:
        object:
          type: string
          enum:
            - log_entry
        id:
          type: string
          example: log_3kH9sQ2pVb7LmXz1
        livemode:
          type: boolean
        occurred_at:
          type: string
          format: date-time
          description: Millisecond precision.
        source:
          type: string
          enum:
            - stripe
            - api
            - nochurn
            - widget
            - webhook
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
            - intercom
            - crisp
            - zendesk
            - hubspot
            - helpscout
            - gorgias
          description: "stripe (webhooks), api (your calls with an API key), nochurn (decisions), widget, webhook (outgoing), a billing tool (chargebee, zuora…: its webhooks and what NoChurn reads from it), a support tool (intercom, crisp, zendesk, hubspot, helpscout, gorgias)."
        type:
          type: string
          example: invoice.payment_failed
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
          properties:
            id:
              type: string
              example: cst_4fJ8sK2pQ9wLm3Xz
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
          nullable: true
          description: The customer this line is about (open it with /v1/customers/{id}).
        summary:
          type: string
          nullable: true
          example: $49.00 · insufficient_funds
        status:
          type: string
          example: received
          description: HTTP status ("200", "500") or ok, received, ignored, failed.
        error:
          type: boolean
        why:
          type: string
          nullable: true
          description: Why this happened (the reason of the NoChurn decision).
        ref:
          type: object
          additionalProperties: false
          required:
            - type
            - id
          properties:
            type:
              type: string
              example: event
            id:
              type: string
              example: nevt_tZCYqtEDVAvwZjD5
          nullable: true
        request_id:
          type: string
          nullable: true
      nullable: false
    LogEntryPage:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
        - poll_cursor
        - retention_days
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/LogEntry"
        has_more:
          type: boolean
          description: "With after=: more new lines are waiting, poll again now."
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get older lines.
        poll_cursor:
          type: string
          nullable: true
          description: Pass as after to get only newer lines (live view).
        retention_days:
          type: integer
          example: 90
      nullable: false
    LogStats:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - timezone
        - today_count
        - errors_count
        - retention_days
      properties:
        object:
          type: string
          enum:
            - log_stats
        livemode:
          type: boolean
        timezone:
          type: string
          example: Europe/Paris
        today_count:
          type: integer
        errors_count:
          type: integer
          minimum: 0
          example: 3
          description: "Error lines in the window and filters passed (same as GET /v1/logs: 24 hours by default)."
        retention_days:
          type: integer
          example: 90
      nullable: false
    PaymentMethodSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - type
        - brand
        - last4
        - exp_month
        - exp_year
        - funding
        - wallet
        - risk_category
        - label
      properties:
        id:
          type: string
          example: npm_7Gh2kL9pQs3VbX1z
        type:
          type: string
          example: card
        brand:
          type: string
          nullable: true
          example: visa
        last4:
          type: string
          nullable: true
          example: "4242"
        exp_month:
          type: integer
          nullable: true
        exp_year:
          type: integer
          nullable: true
        funding:
          type: string
          nullable: true
          example: prepaid
        wallet:
          type: string
          nullable: true
          example: apple_pay
        risk_category:
          type: string
          nullable: true
          enum:
            - bank_card
            - wallet
            - link
            - sepa
            - neobank
            - prepaid
            - disposable
            - null
        label:
          type: string
          example: Apple Pay · Visa •••• 4242
      nullable: true
      description: Default payment method (null when none).
    CustomerPage:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Customer"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
        total_count:
          type: integer
          description: With include[]=total_count.
      nullable: false
    CustomerSummary:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - timezone
        - month_start
        - active
        - high_risk
        - in_recovery
        - saved_this_month
      properties:
        object:
          type: string
          enum:
            - customer_summary
        livemode:
          type: boolean
        timezone:
          type: string
          example: Europe/Paris
        month_start:
          type: string
          format: date-time
        active:
          type: object
          additionalProperties: false
          required:
            - count
            - new_this_month
          properties:
            count:
              type: integer
            new_this_month:
              type: integer
          nullable: false
        high_risk:
          type: object
          additionalProperties: false
          required:
            - threshold
            - count
            - mrr
            - scored_customers
          properties:
            threshold:
              type: integer
            count:
              type: integer
            mrr:
              type: array
              description: One entry per currency, largest first. Never converted.
              items:
                type: object
                additionalProperties: false
                required:
                  - currency
                  - cents
                properties:
                  currency:
                    type: string
                    example: eur
                  cents:
                    type: integer
                nullable: false
            scored_customers:
              type: integer
              description: Active customers with a score.
          nullable: false
        in_recovery:
          type: object
          additionalProperties: false
          required:
            - count
            - mrr
          properties:
            count:
              type: integer
            mrr:
              type: array
              description: One entry per currency, largest first. Never converted.
              items:
                type: object
                additionalProperties: false
                required:
                  - currency
                  - cents
                properties:
                  currency:
                    type: string
                    example: eur
                  cents:
                    type: integer
                nullable: false
          nullable: false
        saved_this_month:
          type: object
          additionalProperties: false
          required:
            - count
            - unavailable_reason
          properties:
            count:
              type: integer
              nullable: true
            unavailable_reason:
              type: string
              nullable: true
          nullable: false
      nullable: false
    CustomerProfile:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - billing
        - score
        - ai_analysis
        - ai_analysis_status
        - attributes
        - links
      properties:
        object:
          type: string
          enum:
            - customer_profile
        customer:
          $ref: "#/components/schemas/Customer"
        billing:
          type: object
          additionalProperties: false
          required:
            - mrr_cents
            - currency
            - lifetime_value_cents
            - lifetime_value_since
            - customer_since
            - payment_method
            - next_renewal
            - subscriptions
          properties:
            mrr_cents:
              type: integer
            currency:
              type: string
              nullable: true
            lifetime_value_cents:
              type: integer
            lifetime_value_since:
              type: string
              format: date-time
              nullable: true
              description: Start of the history the lifetime value covers.
            customer_since:
              type: string
              format: date-time
            payment_method:
              $ref: "#/components/schemas/PaymentMethodSummary"
            next_renewal:
              type: object
              additionalProperties: false
              required:
                - at
                - amount_cents
                - currency
                - subscription
              properties:
                at:
                  type: string
                  format: date-time
                amount_cents:
                  type: integer
                currency:
                  type: string
                  nullable: true
                subscription:
                  type: string
              nullable: true
            subscriptions:
              type: array
              items:
                type: object
                additionalProperties: false
                required:
                  - id
                  - plan
                  - status
                  - mrr_cents
                  - currency
                  - interval
                  - interval_count
                  - current_period_end
                  - cancel_at_period_end
                properties:
                  id:
                    type: string
                  plan:
                    type: string
                    nullable: true
                  status:
                    type: string
                  mrr_cents:
                    type: integer
                  currency:
                    type: string
                    nullable: true
                  interval:
                    type: string
                    nullable: true
                  interval_count:
                    type: integer
                  current_period_end:
                    type: string
                    format: date-time
                    nullable: true
                  cancel_at_period_end:
                    type: boolean
                nullable: false
          nullable: false
        score:
          type: object
          additionalProperties: false
          required:
            - score
            - level
            - factors
            - computed_at
          properties:
            score:
              type: integer
              nullable: true
            level:
              type: string
              nullable: true
            factors:
              type: array
              items:
                type: object
                additionalProperties: false
                required:
                  - key
                  - label
                  - points
                properties:
                  key:
                    type: string
                  label:
                    type: string
                  points:
                    type: integer
                nullable: false
            computed_at:
              type: string
              format: date-time
              nullable: true
          nullable: false
          description: Factors are computed with the churn score (M4).
        ai_analysis:
          $ref: "#/components/schemas/CustomerInsight"
        ai_analysis_status:
          type: string
          enum:
            - fresh
            - stale
            - missing
            - not_enough_data
          description: "fresh: less than 7 days old and no important event since (POST …/insight returns it without an AI call). stale: older, or an important event came after it (ai_analysis is the old one). missing: never analysed. not_enough_data: too little activity for an analysis (POST …/insight answers 422 insufficient_data)."
        attributes:
          type: object
          additionalProperties: true
        links:
          type: object
          additionalProperties: false
          required:
            - stripe_dashboard
          properties:
            stripe_dashboard:
              type: string
              nullable: true
              format: uri
          nullable: false
      nullable: false
    TimelinePage:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - object
              - id
              - occurred_at
              - category
              - type
              - summary
              - ref
              - data
            properties:
              object:
                type: string
                enum:
                  - timeline_item
              id:
                type: string
                example: npay_2Lk9sQ7vBn3XmP4a
              occurred_at:
                type: string
                format: date-time
              category:
                type: string
                enum:
                  - billing
                  - flow
                  - support
                  - product
                  - account
              type:
                type: string
                example: payment.failed
              summary:
                type: string
                nullable: true
                example: €49.00 · failed · insufficient_funds
              ref:
                type: object
                additionalProperties: false
                required:
                  - type
                  - id
                properties:
                  type:
                    type: string
                    nullable: true
                  id:
                    type: string
                nullable: true
              data:
                type: object
                additionalProperties: true
            nullable: false
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
      nullable: false
    Flow:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - kind
        - key
        - name
        - status
        - holdout_percent
        - test_version
        - live_version
        - unpublished_changes
        - mode_status
        - draft_updated_at
        - paused_at
        - created_at
        - updated_at
      properties:
        object:
          type: string
          enum:
            - flow
        id:
          type: string
          example: flw_3Ls9dK2mQx7VbN4p
        kind:
          type: string
          enum:
            - prevention
            - recovery
            - cancel
            - winback
        key:
          type: string
          example: default
        name:
          type: string
          example: Recovery flow
        status:
          type: string
          enum:
            - draft
            - test
            - live
            - paused
          description: "draft: never published; test: runs in test mode only; live: runs in both modes; paused."
        holdout_percent:
          type: integer
          description: Share of customers NoChurn leaves alone, to measure its effect.
        test_version:
          type: object
          additionalProperties: false
          required:
            - id
            - number
            - published_at
            - published_to
          properties:
            id:
              type: string
              example: flv_9Kd2mX7pQs4LzA1b
            number:
              type: integer
            published_at:
              type: string
              format: date-time
            published_to:
              type: string
              enum:
                - test
                - live
          nullable: true
        live_version:
          type: object
          additionalProperties: false
          required:
            - id
            - number
            - published_at
            - published_to
          properties:
            id:
              type: string
              example: flv_9Kd2mX7pQs4LzA1b
            number:
              type: integer
            published_at:
              type: string
              format: date-time
            published_to:
              type: string
              enum:
                - test
                - live
          nullable: true
        unpublished_changes:
          type: array
          items:
            type: string
          description: Draft sections that differ from the last version.
        mode_status:
          type: object
          additionalProperties: false
          required:
            - livemode
            - state
            - version
            - published_at
            - unpublished_changes
          properties:
            livemode:
              type: boolean
            state:
              type: string
              enum:
                - published
                - draft
                - paused
              description: "published: a version runs in this mode; draft: nothing published in this mode yet; paused: paused (both modes)."
            version:
              type: integer
              nullable: true
              description: Number of the version that runs in this mode.
            published_at:
              type: string
              format: date-time
              nullable: true
            unpublished_changes:
              type: array
              items:
                type: string
              description: Draft sections not yet published in this mode (a pending draft).
          nullable: false
          description: Where the flow stands in the mode of the request (NoChurn-Mode or the key's mode).
        draft_updated_at:
          type: string
          format: date-time
          nullable: true
        paused_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      nullable: false
    FlowDetail:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - kind
        - key
        - name
        - status
        - holdout_percent
        - test_version
        - live_version
        - unpublished_changes
        - mode_status
        - draft_updated_at
        - paused_at
        - created_at
        - updated_at
        - draft
        - logos
      properties:
        object:
          type: string
          enum:
            - flow
        id:
          type: string
          example: flw_3Ls9dK2mQx7VbN4p
        kind:
          type: string
          enum:
            - prevention
            - recovery
            - cancel
            - winback
        key:
          type: string
          example: default
        name:
          type: string
          example: Recovery flow
        status:
          type: string
          enum:
            - draft
            - test
            - live
            - paused
          description: "draft: never published; test: runs in test mode only; live: runs in both modes; paused."
        holdout_percent:
          type: integer
          description: Share of customers NoChurn leaves alone, to measure its effect.
        test_version:
          type: object
          additionalProperties: false
          required:
            - id
            - number
            - published_at
            - published_to
          properties:
            id:
              type: string
              example: flv_9Kd2mX7pQs4LzA1b
            number:
              type: integer
            published_at:
              type: string
              format: date-time
            published_to:
              type: string
              enum:
                - test
                - live
          nullable: true
        live_version:
          type: object
          additionalProperties: false
          required:
            - id
            - number
            - published_at
            - published_to
          properties:
            id:
              type: string
              example: flv_9Kd2mX7pQs4LzA1b
            number:
              type: integer
            published_at:
              type: string
              format: date-time
            published_to:
              type: string
              enum:
                - test
                - live
          nullable: true
        unpublished_changes:
          type: array
          items:
            type: string
          description: Draft sections that differ from the last version.
        mode_status:
          type: object
          additionalProperties: false
          required:
            - livemode
            - state
            - version
            - published_at
            - unpublished_changes
          properties:
            livemode:
              type: boolean
            state:
              type: string
              enum:
                - published
                - draft
                - paused
              description: "published: a version runs in this mode; draft: nothing published in this mode yet; paused: paused (both modes)."
            version:
              type: integer
              nullable: true
              description: Number of the version that runs in this mode.
            published_at:
              type: string
              format: date-time
              nullable: true
            unpublished_changes:
              type: array
              items:
                type: string
              description: Draft sections not yet published in this mode (a pending draft).
          nullable: false
          description: Where the flow stands in the mode of the request (NoChurn-Mode or the key's mode).
        draft_updated_at:
          type: string
          format: date-time
          nullable: true
        paused_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        draft:
          type: object
          additionalProperties: true
          description: Configuration being edited, by section.
        logos:
          type: array
          items:
            $ref: "#/components/schemas/CancelFlowLogo"
          description: Logos uploaded for the cancel modal and not removed (POST /v1/flows/cancel/logos); [] for the other flows.
      nullable: false
    FlowVersion:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - flow
        - number
        - config
        - holdout_percent
        - published_to
        - published_by
        - published_at
      properties:
        object:
          type: string
          enum:
            - flow_version
        id:
          type: string
        flow:
          type: string
        number:
          type: integer
        config:
          type: object
          additionalProperties: true
        holdout_percent:
          type: integer
        published_to:
          type: string
          enum:
            - test
            - live
        published_by:
          type: string
          nullable: true
        published_at:
          type: string
          format: date-time
      nullable: false
    FlowRun:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - flow
        - version
        - customer
        - subject
        - holdout
        - state
        - current_step
        - outcome
        - outcome_at
        - outcome_mrr_cents
        - outcome_cents
        - currency
        - confirmation
        - stop_reason
        - started_at
        - ended_at
      properties:
        object:
          type: string
          enum:
            - flow_run
        id:
          type: string
          example: run_7Hk2sQ9vLp3XmB4a
        livemode:
          type: boolean
        flow:
          type: string
        version:
          type: integer
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
            - mrr_cents
            - currency
          properties:
            id:
              type: string
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
            mrr_cents:
              type: integer
            currency:
              type: string
              nullable: true
          nullable: true
        subject:
          type: object
          additionalProperties: false
          required:
            - type
            - id
          properties:
            type:
              type: string
              nullable: true
            id:
              type: string
          nullable: true
        holdout:
          type: boolean
          description: "In the holdout group: NoChurn does nothing, the run is only measured."
        state:
          type: string
          enum:
            - active
            - succeeded
            - failed
            - stopped
        current_step:
          type: string
          nullable: true
        outcome:
          type: string
          nullable: true
          enum:
            - prevented
            - recovered
            - saved
            - won_back
            - lost
            - cancelled
            - abandoned
            - null
        outcome_at:
          type: string
          format: date-time
          nullable: true
        outcome_mrr_cents:
          type: integer
          nullable: true
        outcome_cents:
          type: integer
          nullable: true
        currency:
          type: string
          nullable: true
        confirmation:
          type: string
          nullable: true
          enum:
            - pending
            - confirmed
            - reverted
            - null
          description: pending, then confirmed if the customer still pays 30 days later, else reverted.
        stop_reason:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
        ended_at:
          type: string
          format: date-time
          nullable: true
      nullable: false
    SendSchedule:
      type: object
      additionalProperties: false
      required:
        - object
        - audience
        - overrides
        - effective
        - presets
      properties:
        object:
          type: string
          enum:
            - send_schedule
        audience:
          type: string
          nullable: true
          enum:
            - b2b
            - b2c
            - null
        overrides:
          type: object
          additionalProperties: true
          description: The account's own options on top of the preset.
          properties:
            audience:
              type: string
              enum:
                - b2b
                - b2c
            respect_customer_timezone:
              type: boolean
            skip_weekends:
              type: boolean
            skip_public_holidays:
              type: boolean
              description: Holidays of the customer's country (and state).
            windows:
              type: object
              properties:
                weekday:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
                weekend:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
            quiet_hours:
              type: object
              properties:
                enabled:
                  type: boolean
                from:
                  type: string
                to:
                  type: string
            days_off:
              type: array
              items:
                type: string
                format: date
        effective:
          type: object
          additionalProperties: true
          description: What applies (before a flow's own overrides).
          properties:
            audience:
              type: string
              enum:
                - b2b
                - b2c
            respect_customer_timezone:
              type: boolean
            skip_weekends:
              type: boolean
            skip_public_holidays:
              type: boolean
              description: Holidays of the customer's country (and state).
            windows:
              type: object
              properties:
                weekday:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
                weekend:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
            quiet_hours:
              type: object
              properties:
                enabled:
                  type: boolean
                from:
                  type: string
                to:
                  type: string
            days_off:
              type: array
              items:
                type: string
                format: date
        presets:
          type: object
          required:
            - b2b
            - b2c
          additionalProperties: false
          description: "Every option's default, per audience (the preset, then the defaults common to both: time zone, quiet hours, days off). An option equal to it is sent as null to follow the preset."
          properties:
            b2b:
              type: object
              additionalProperties: true
              description: B2B preset.
              properties:
                audience:
                  type: string
                  enum:
                    - b2b
                    - b2c
                respect_customer_timezone:
                  type: boolean
                skip_weekends:
                  type: boolean
                skip_public_holidays:
                  type: boolean
                  description: Holidays of the customer's country (and state).
                windows:
                  type: object
                  properties:
                    weekday:
                      type: array
                      items:
                        type: array
                        items:
                          type: string
                    weekend:
                      type: array
                      items:
                        type: array
                        items:
                          type: string
                quiet_hours:
                  type: object
                  properties:
                    enabled:
                      type: boolean
                    from:
                      type: string
                    to:
                      type: string
                days_off:
                  type: array
                  items:
                    type: string
                    format: date
              required:
                - respect_customer_timezone
                - skip_weekends
                - skip_public_holidays
                - windows
                - quiet_hours
                - days_off
            b2c:
              type: object
              additionalProperties: true
              description: B2C preset.
              properties:
                audience:
                  type: string
                  enum:
                    - b2b
                    - b2c
                respect_customer_timezone:
                  type: boolean
                skip_weekends:
                  type: boolean
                skip_public_holidays:
                  type: boolean
                  description: Holidays of the customer's country (and state).
                windows:
                  type: object
                  properties:
                    weekday:
                      type: array
                      items:
                        type: array
                        items:
                          type: string
                    weekend:
                      type: array
                      items:
                        type: array
                        items:
                          type: string
                quiet_hours:
                  type: object
                  properties:
                    enabled:
                      type: boolean
                    from:
                      type: string
                    to:
                      type: string
                days_off:
                  type: array
                  items:
                    type: string
                    format: date
              required:
                - respect_customer_timezone
                - skip_weekends
                - skip_public_holidays
                - windows
                - quiet_hours
                - days_off
        suggested_audience:
          type: object
          additionalProperties: false
          required:
            - audience
            - reason
            - sample_size
            - consumer_share
          properties:
            audience:
              type: string
              enum:
                - b2b
                - b2c
            reason:
              type: string
              example: 82% of your 140 customers use company addresses
            sample_size:
              type: integer
            consumer_share:
              type: number
              example: 0.18
          nullable: true
          description: Suggestion from the customers' email domains (onboarding step 1); null below 20 customers with an email.
      nullable: false
    SendSlot:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - at
        - timezone
        - timezone_source
        - delayed
        - reasons
        - audience
      properties:
        object:
          type: string
          enum:
            - send_slot
        customer:
          type: string
        at:
          type: string
          format: date-time
          nullable: true
          description: null when the send window allows no moment in the next 31 days
        timezone:
          type: string
          example: Europe/Berlin
        timezone_source:
          type: string
          nullable: true
        delayed:
          type: boolean
        reasons:
          type: array
          items:
            type: string
          example:
            - weekend
        audience:
          type: string
          enum:
            - b2b
            - b2c
      nullable: false
    SendingDomain:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - domain
        - sender_name
        - sender_email
        - sending_host
        - status
        - provider
        - provisioning_error
        - verified_at
        - last_checked_at
        - existing_dmarc
        - fallback_address
        - from_address
        - dns_records
        - created_at
      properties:
        object:
          type: string
          enum:
            - sending_domain
        id:
          type: string
          example: sdm_4Hs8dK2mQx7VbN4p
        domain:
          type: string
          example: acme.com
        sender_name:
          type: string
          nullable: true
        sender_email:
          type: string
          example: billing@acme.com
        sending_host:
          type: string
          example: nc-x7k2.acme.com
        status:
          type: string
          enum:
            - provisioning
            - pending
            - verified
            - failed
          description: "provisioning: Mailgun is preparing the records (dns_records is empty until then, or provisioning_error says why it could not); pending: records to add; verified: records found and the domain active at Mailgun; failed: records still missing after 7 days."
        provider:
          type: string
          enum:
            - mailgun
            - fake
          description: "Who holds the domain: mailgun, or fake outside production (sample records, nothing created)."
        provisioning_error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - fix
            - docs_url
          properties:
            code:
              type: string
              enum:
                - email_domain_provisioning_failed
            message:
              type: string
            fix:
              type: string
            docs_url:
              type: string
          nullable: true
          description: Why the records could not be prepared yet, with what to do; null otherwise.
        verified_at:
          type: string
          format: date-time
          nullable: true
        last_checked_at:
          type: string
          format: date-time
          nullable: true
        existing_dmarc:
          type: boolean
          description: "A DMARC record already exists: NoChurn does not propose another one."
        fallback_address:
          type: string
          example: acme@mail.nochurn.sh
        from_address:
          type: string
          description: "Emails go out from this address now: the sender once verified, the fallback before."
        dns_records:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - type
              - host
              - value
              - purpose
              - optional
              - status
              - found
              - checked_at
            properties:
              type:
                type: string
                enum:
                  - MX
                  - TXT
                  - CNAME
              host:
                type: string
                example: nc-x7k2.acme.com
              value:
                type: string
                example: 10 mxa.eu.mailgun.org
              purpose:
                type: string
                example: replies, bounces
              optional:
                type: boolean
              status:
                type: string
                enum:
                  - pending
                  - verified
              found:
                type: string
                nullable: true
              checked_at:
                type: string
                format: date-time
                nullable: true
            nullable: false
        created_at:
          type: string
          format: date-time
      nullable: false
    Message:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - channel
        - customer
        - flow_run
        - template_key
        - locale
        - from
        - from_name
        - reply_to
        - to
        - subject
        - ai_generated
        - review_state
        - review_issues
        - reviewed_at
        - status
        - scheduled_for
        - schedule_reasons
        - sent_at
        - delivered_at
        - opened_at
        - clicked_at
        - bounced_at
        - error
        - created_at
      properties:
        object:
          type: string
          enum:
            - message
        id:
          type: string
          example: msg_8Hd2kQ9xLm3VbN7p
        livemode:
          type: boolean
        channel:
          type: string
          enum:
            - email
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
          properties:
            id:
              type: string
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
          nullable: false
        flow_run:
          type: string
          nullable: true
        template_key:
          type: string
          nullable: true
        locale:
          type: string
          nullable: true
        from:
          type: string
          example: billing@acme.com
        from_name:
          type: string
          nullable: true
        reply_to:
          type: string
          nullable: true
        to:
          type: string
        subject:
          type: string
        ai_generated:
          type: boolean
        review_state:
          type: string
          enum:
            - auto_approved
            - needs_review
            - approved
            - rejected
        review_issues:
          type: array
          items:
            type: string
          description: Why the automatic review kept it for a human.
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum:
            - queued
            - sending
            - sent
            - delivered
            - bounced
            - complained
            - failed
            - suppressed
            - cancelled
        scheduled_for:
          type: string
          format: date-time
        schedule_reasons:
          type: array
          items:
            type: string
        sent_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        opened_at:
          type: string
          format: date-time
          nullable: true
        clicked_at:
          type: string
          format: date-time
          nullable: true
        bounced_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    MessageDetail:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - channel
        - customer
        - flow_run
        - template_key
        - locale
        - from
        - from_name
        - reply_to
        - to
        - subject
        - ai_generated
        - review_state
        - review_issues
        - reviewed_at
        - status
        - scheduled_for
        - schedule_reasons
        - sent_at
        - delivered_at
        - opened_at
        - clicked_at
        - bounced_at
        - error
        - created_at
        - body_text
        - body_html
      properties:
        object:
          type: string
          enum:
            - message
        id:
          type: string
          example: msg_8Hd2kQ9xLm3VbN7p
        livemode:
          type: boolean
        channel:
          type: string
          enum:
            - email
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
          properties:
            id:
              type: string
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
          nullable: false
        flow_run:
          type: string
          nullable: true
        template_key:
          type: string
          nullable: true
        locale:
          type: string
          nullable: true
        from:
          type: string
          example: billing@acme.com
        from_name:
          type: string
          nullable: true
        reply_to:
          type: string
          nullable: true
        to:
          type: string
        subject:
          type: string
        ai_generated:
          type: boolean
        review_state:
          type: string
          enum:
            - auto_approved
            - needs_review
            - approved
            - rejected
        review_issues:
          type: array
          items:
            type: string
          description: Why the automatic review kept it for a human.
        reviewed_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum:
            - queued
            - sending
            - sent
            - delivered
            - bounced
            - complained
            - failed
            - suppressed
            - cancelled
        scheduled_for:
          type: string
          format: date-time
        schedule_reasons:
          type: array
          items:
            type: string
        sent_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        opened_at:
          type: string
          format: date-time
          nullable: true
        clicked_at:
          type: string
          format: date-time
          nullable: true
        bounced_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        body_text:
          type: string
        body_html:
          type: string
          nullable: true
      nullable: false
    EmailSuppression:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - email
        - reason
        - scope
        - message
        - created_at
      properties:
        object:
          type: string
          enum:
            - email_suppression
        id:
          type: string
          example: sup_2Kd9xLm3VbN7pQ4s
        livemode:
          type: boolean
        email:
          type: string
        reason:
          type: string
          enum:
            - unsubscribed
            - bounced
            - complained
            - gdpr
            - manual
        scope:
          type: string
          enum:
            - all
            - marketing
        message:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    SetupShare:
      type: object
      additionalProperties: false
      required:
        - object
        - topic
        - email
        - sent_at
      properties:
        object:
          type: string
          enum:
            - setup_share
        topic:
          type: string
          enum:
            - email_domain
            - api
            - cancel_modal
        email:
          type: string
        sent_at:
          type: string
          format: date-time
      nullable: false
    FlowTestEmail:
      type: object
      additionalProperties: false
      required:
        - object
        - to
        - subject
        - body
        - ai_generated
        - source
        - review
      properties:
        object:
          type: string
          enum:
            - flow_test_email
        to:
          type: string
        subject:
          type: string
          example: "[TEST] Your payment didn't go through"
        body:
          type: string
        ai_generated:
          type: boolean
        source:
          type: string
          enum:
            - custom
            - ai
            - template
          description: "custom: your text; ai: written by the AI; template: NoChurn's text."
        review:
          type: object
          additionalProperties: false
          required:
            - state
            - issues
          properties:
            state:
              type: string
              enum:
                - auto_approved
                - needs_review
            issues:
              type: array
              items:
                type: string
          nullable: false
      nullable: false
    EmailPreview:
      type: object
      additionalProperties: false
      required:
        - object
        - step
        - label
        - language
        - source
        - subject
        - text
        - html
        - ai_generated
        - review
        - warnings
        - customer
        - sample
        - template
        - custom
        - placeholders
        - required_placeholders
        - languages
        - sources
      properties:
        object:
          type: string
          enum:
            - email_preview
        step:
          type: string
          example: email_1
          description: email_1…, card_removed, card_expiring, renewal_reminder, d30, d60, d90, review_follow_up
        label:
          type: string
          example: Email 1 · day 0
        language:
          type: string
          example: en
        source:
          type: string
          enum:
            - custom
            - ai
            - template
          description: Where the text shown comes from.
        subject:
          type: string
          description: Placeholders filled.
        text:
          type: string
          description: Plain-text body, placeholders filled.
        html:
          type: string
          description: HTML body as sent (escaped paragraphs and links, no script, no style).
        ai_generated:
          type: boolean
        review:
          type: object
          additionalProperties: false
          required:
            - state
            - issues
          properties:
            state:
              type: string
              enum:
                - auto_approved
                - needs_review
            issues:
              type: array
              items:
                type: string
          nullable: false
        warnings:
          type: array
          items:
            type: string
          description: What the preview differs on, and problems of your text.
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - example
          properties:
            id:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
            example:
              type: boolean
          nullable: false
          description: "Who the placeholders were filled for: the example customer (fictional data) or a real customer."
        sample:
          type: boolean
          description: True when the data is the example customer's.
        template:
          type: object
          additionalProperties: false
          required:
            - subject
            - body
          properties:
            subject:
              type: string
            body:
              type: string
          nullable: false
          description: NoChurn's text in this language, placeholders not filled (to start from or reset to).
        custom:
          type: object
          additionalProperties: false
          required:
            - subject
            - body
          properties:
            subject:
              type: string
            body:
              type: string
          nullable: true
          description: Your text saved in the draft for this language, or null.
        placeholders:
          type: array
          items:
            type: string
          example:
            - first_name
            - amount
            - company
            - card_link
            - sender_name
        required_placeholders:
          type: array
          items:
            type: string
          example:
            - card_link
        languages:
          type: array
          items:
            type: string
          example:
            - en
            - fr
        sources:
          type: array
          items:
            type: string
            enum:
              - custom
              - ai
              - template
      nullable: false
    InappState:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - failing_since
        - access
        - access_reason
        - locale
        - texts
        - theme
        - banner
        - wall
      properties:
        object:
          type: string
          enum:
            - inapp_state
        customer:
          type: string
        failing_since:
          type: string
          format: date-time
          nullable: true
        access:
          type: string
          enum:
            - full
            - restricted
          description: "restricted exactly when wall.show is true: the same calculation as GET /v1/customers/{id}/access (DECISIONS 81, 87)."
        access_reason:
          type: string
          enum:
            - payment_failed
            - paused
            - null
          nullable: true
          description: "Why access is restricted; null when full. paused: only when the cancel flow's pause.inapp is wall."
        locale:
          type: string
          enum:
            - en
            - fr
            - de
            - it
            - pt
          description: ?locale=, then the customer's language, then the account's.
        texts:
          type: object
          nullable: true
          additionalProperties:
            type: string
          description: "Banner and wall texts in this locale (banner_text, banner_cta, dismiss, banner_days_left, banner_last_day, wall_title, wall_text, wall_cta, pay_cta, amount_due, card_expired, decline_expired, decline_insufficient_funds, decline_authentication_required, decline_declined, checking_payment, back_in_title, back_in_text, data_safe, paused_banner_text, paused_title, paused_text, paused_resumes, resume_cta, resuming, resumed_title, resumed_text, cancel_now): the recovery flow's inapp.texts, else NoChurn's. {{amount}} is already filled (the unpaid amount, or the regular price when paused); {{days}}, {{date}} and {{exp}} are for the widget to fill (from details, days_left, in the customer's locale). Null when nothing is shown."
        theme:
          allOf:
            - $ref: "#/components/schemas/CancelFlowTheme"
          nullable: true
          description: The cancel flow's design (same tokens as the modal). Null when nothing is shown.
        banner:
          type: object
          additionalProperties: false
          required:
            - show
          properties:
            show:
              type: boolean
            text:
              type: string
            cta_url:
              type: string
              nullable: true
              example: https://pay.nochurn.sh/c/…
            cta_label:
              type: string
              nullable: true
            kind:
              type: string
              enum:
                - payment_failed
                - paused
            audience:
              type: string
              enum:
                - admins
                - everyone
            dismissible_for_hours:
              type: integer
            dismiss_label:
              type: string
            restricts_at:
              type: string
              format: date-time
              nullable: true
              description: "payment_failed: when the wall comes up if still unpaid (wall enabled, not up yet); else null."
            days_left:
              type: integer
              nullable: true
              minimum: 0
              description: Whole days until restricts_at, in the customer's time zone (0 = last day); null with restricts_at.
            details:
              oneOf:
                - $ref: "#/components/schemas/InappPaymentDetails"
                - $ref: "#/components/schemas/InappPausedDetails"
              nullable: true
              description: The facts the banner draws (same as wall.details); null when the banner is not shown.
        wall:
          type: object
          additionalProperties: false
          required:
            - show
          properties:
            show:
              type: boolean
            text:
              type: string
            cta_url:
              type: string
              nullable: true
              example: https://pay.nochurn.sh/c/…
            cta_label:
              type: string
              nullable: true
            kind:
              type: string
              enum:
                - payment_failed
                - paused
            shows_after_days:
              type: integer
              nullable: true
              description: "payment_failed: the grace in days; null for paused (at once)."
            cancel:
              type: object
              additionalProperties: false
              required:
                - label
                - fallback_url
              properties:
                label:
                  type: string
                  description: The cancel flow's cancel_now text (or the in-app override).
                fallback_url:
                  type: string
                  nullable: true
                  description: The publishable key's fallback cancel link (null for a secret key).
              nullable: false
              description: "Click to cancel on the wall: open the cancel modal (NoChurn.cancel, POST /v1/cancel_sessions); if that fails, POST the customer token as a form field `token` to fallback_url."
            details:
              oneOf:
                - $ref: "#/components/schemas/InappPaymentDetails"
                - $ref: "#/components/schemas/InappPausedDetails"
              nullable: true
              description: "The facts the wall draws; null when the wall is not shown. paused: cta_label is the resume button (POST /v1/inapp/resume), null when resume_allowed is false; cta_url is null."
      nullable: false
    InappPaymentDetails:
      type: object
      additionalProperties: false
      required:
        - kind
        - amount_due_cents
        - currency
        - invoice_due_at
        - payment_method
        - decline
      properties:
        kind:
          type: string
          enum:
            - payment_failed
        amount_due_cents:
          type: integer
        currency:
          type: string
          nullable: true
          example: eur
        invoice_due_at:
          type: string
          format: date-time
          nullable: true
          description: The invoice's due date when the billing set one, else null.
        payment_method:
          type: object
          additionalProperties: false
          required:
            - brand
            - last4
            - exp_month
            - exp_year
          properties:
            brand:
              type: string
              nullable: true
              example: visa
            last4:
              type: string
              nullable: true
              example: "4242"
            exp_month:
              type: integer
              nullable: true
            exp_year:
              type: integer
              nullable: true
          nullable: true
          description: The method of the failed payment, else the customer's default one; null when unknown.
        decline:
          type: string
          enum:
            - expired
            - insufficient_funds
            - authentication_required
            - declined
          description: Mapped from the provider's decline or failure code (expired_card → expired; insufficient_funds; authentication_required; anything else → declined).
      nullable: false
      description: "kind payment_failed: what the payment wall draws. Never more card data than brand, last4 and expiry; never the provider's message."
    InappPausedDetails:
      type: object
      additionalProperties: false
      required:
        - kind
        - paused_at
        - resumes_at
        - regular_amount_cents
        - currency
        - resume_allowed
      properties:
        kind:
          type: string
          enum:
            - paused
        paused_at:
          type: string
          format: date-time
          nullable: true
          description: When the pause started (null when NoChurn never saw it start).
        resumes_at:
          type: string
          format: date-time
          nullable: true
          description: "When it resumes by itself (null: no date set)."
        regular_amount_cents:
          type: integer
          description: The price per interval once resumed (quantity included).
        currency:
          type: string
          nullable: true
          example: eur
        resume_allowed:
          type: boolean
          description: POST /v1/inapp/resume can resume it (Stripe, or a provider NoChurn resumes); false for billing by API.
      nullable: false
      description: "kind paused: what the paused wall or banner draws."
    CancelSessionResult:
      type: object
      additionalProperties: false
      required:
        - kind
        - currency
        - regular_amount_cents
        - next_invoice_at
        - next_invoice_amount_cents
        - discounted_until
        - discounted_invoices
        - resumes_at
        - plan_name
      properties:
        kind:
          type: string
          enum:
            - discount
            - pause
            - downgrade
            - talk
        currency:
          type: string
          nullable: true
          example: eur
        regular_amount_cents:
          type: integer
          nullable: true
          description: The price per interval before the offer (quantity included). Null for talk.
        next_invoice_at:
          type: string
          format: date-time
          nullable: true
          description: "discount, downgrade: the next invoice's date (the provider's, else the renewal date)."
        next_invoice_amount_cents:
          type: integer
          nullable: true
          description: "discount, downgrade: the next invoice's amount from the provider (Stripe: upcoming invoice preview, read once when the offer is applied). Null when the provider cannot tell."
        discounted_until:
          type: string
          format: date-time
          nullable: true
          description: "discount: when the usual price comes back (acceptance + the offer's months)."
        discounted_invoices:
          type: array
          maxItems: 12
          description: "discount: the invoices due before discounted_until (12 at most), at next_invoice_amount_cents. [] otherwise."
          items:
            type: object
            additionalProperties: false
            required:
              - at
              - amount_cents
            properties:
              at:
                type: string
                format: date-time
              amount_cents:
                type: integer
                nullable: true
            nullable: false
        resumes_at:
          type: string
          format: date-time
          nullable: true
          description: "pause: the resume date (null when your billing pauses it, billing by API)."
        plan_name:
          type: string
          nullable: true
          description: "downgrade: the new plan, when the offer names it."
      nullable: false
      description: "The end screen of a saved customer. Never an invented number: unknown = null."
    CustomerAccess:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - livemode
        - access
        - reason
        - since
        - failing_since
        - restricts_at
        - invoice
        - pay_url
        - subscription
        - paused_at
        - resumes_at
        - cancel_url
      properties:
        object:
          type: string
          enum:
            - customer_access
        customer:
          type: string
          example: cst_8Hd2kQ9xLm3VbN7p
        livemode:
          type: boolean
        access:
          type: string
          enum:
            - full
            - restricted
          description: "restricted: block your app (keep paying, resuming and cancelling reachable)."
        reason:
          type: string
          enum:
            - payment_failed
            - paused
            - null
          nullable: true
          description: "Null when full. paused: every live subscription is paused and the cancel flow's pause.inapp is wall."
        since:
          type: string
          format: date-time
          nullable: true
          description: When the restriction started (the failure + the grace, or the start of the pause; null when unknown). Null when full.
        failing_since:
          type: string
          format: date-time
          nullable: true
          description: When the oldest unpaid invoice under recovery started failing.
        restricts_at:
          type: string
          format: date-time
          nullable: true
          description: When access will be restricted if still unpaid (full now, wall enabled); else null.
        invoice:
          type: string
          nullable: true
          description: The unpaid invoice (ninv_…) that sets the clock.
        pay_url:
          type: string
          nullable: true
          description: Where the customer pays or updates their card (null when nothing is failing).
        subscription:
          type: string
          nullable: true
          description: The paused subscription (nsub_…) when the cancel flow shows the pause (banner or wall); else null.
        paused_at:
          type: string
          format: date-time
          nullable: true
          description: "With subscription: when the pause started (null when unknown)."
        resumes_at:
          type: string
          format: date-time
          nullable: true
          description: "With subscription: when it resumes by itself (null: no date)."
        cancel_url:
          type: string
          nullable: true
          description: "Where the customer cancels in one click (cancel.nochurn.sh/a/…, valid 30 days): show it next to pay_url (click to cancel). Null when nothing is failing or paused."
      nullable: false
      description: "May this customer use your app? Same calculation as the in-app wall (wall.after_days, annual_grace_days, wall.enabled of the recovery flow live in this mode; pause.inapp of the cancel flow). Holdout customers, paused flows, a disabled wall or no recovery run: full. Never card data."
    WidgetSecret:
      type: object
      additionalProperties: false
      required:
        - object
        - configured
        - created_at
        - secret
      properties:
        object:
          type: string
          enum:
            - widget_secret
        configured:
          type: boolean
        created_at:
          type: string
          format: date-time
          nullable: true
        secret:
          type: string
          nullable: true
          description: "Only when created or rotated: shown once."
      nullable: false
    WebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - url
        - description
        - events
        - status
        - secret
        - last_status
        - last_delivery_at
        - failure_streak
        - created_at
      properties:
        object:
          type: string
          enum:
            - webhook_endpoint
        id:
          type: string
          example: whk_7Hd2kQ9xLm3VbN7p
        livemode:
          type: boolean
        url:
          type: string
        description:
          type: string
          nullable: true
        events:
          type: array
          items:
            type: string
        status:
          type: string
          enum:
            - enabled
            - disabled
        secret:
          type: string
          nullable: true
          description: "whsec_…: only when created or rotated."
        last_status:
          type: integer
          nullable: true
        last_delivery_at:
          type: string
          format: date-time
          nullable: true
        failure_streak:
          type: integer
        created_at:
          type: string
          format: date-time
      nullable: false
    WebhookDelivery:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - endpoint
        - destination
        - event
        - type
        - status
        - attempts
        - next_attempt_at
        - response_status
        - response_ms
        - error
        - delivered_at
        - failed_at
        - replay_of
        - created_at
      properties:
        object:
          type: string
          enum:
            - webhook_delivery
        id:
          type: string
          example: whd_2Kd9xLm3VbN7pQ4s
        livemode:
          type: boolean
        endpoint:
          type: string
        destination:
          type: string
          enum:
            - http
            - slack
            - intercom_note
          description: "http: your endpoint; slack / intercom_note: a flow notification."
        event:
          type: string
          nullable: true
        type:
          type: string
        status:
          type: string
          enum:
            - pending
            - retrying
            - delivered
            - failed
            - skipped
          description: "skipped: a notification NoChurn chose not to send (no Intercom contact for this customer)."
        attempts:
          type: integer
        next_attempt_at:
          type: string
          format: date-time
          nullable: true
        response_status:
          type: integer
          nullable: true
        response_ms:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        failed_at:
          type: string
          format: date-time
          nullable: true
        replay_of:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    WebhookDeliveryDetail:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - endpoint
        - destination
        - event
        - type
        - status
        - attempts
        - next_attempt_at
        - response_status
        - response_ms
        - error
        - delivered_at
        - failed_at
        - replay_of
        - created_at
        - payload
        - response_excerpt
      properties:
        object:
          type: string
          enum:
            - webhook_delivery
        id:
          type: string
          example: whd_2Kd9xLm3VbN7pQ4s
        livemode:
          type: boolean
        endpoint:
          type: string
        destination:
          type: string
          enum:
            - http
            - slack
            - intercom_note
          description: "http: your endpoint; slack / intercom_note: a flow notification."
        event:
          type: string
          nullable: true
        type:
          type: string
        status:
          type: string
          enum:
            - pending
            - retrying
            - delivered
            - failed
            - skipped
          description: "skipped: a notification NoChurn chose not to send (no Intercom contact for this customer)."
        attempts:
          type: integer
        next_attempt_at:
          type: string
          format: date-time
          nullable: true
        response_status:
          type: integer
          nullable: true
        response_ms:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        failed_at:
          type: string
          format: date-time
          nullable: true
        replay_of:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        payload:
          type: object
          additionalProperties: true
        response_excerpt:
          type: string
          nullable: true
      nullable: false
    Payment:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - customer
        - subscription
        - invoice
        - external_id
        - stripe_charge_id
        - status
        - amount_cents
        - currency
        - decline_code
        - failure_code
        - occurred_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - payment
        id:
          type: string
          example: npay_4Hs8dK2mQx7VbN4p
        livemode:
          type: boolean
        customer:
          type: string
        subscription:
          type: string
          nullable: true
        invoice:
          type: string
          nullable: true
        external_id:
          type: string
          nullable: true
        stripe_charge_id:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - succeeded
            - failed
            - pending
        amount_cents:
          type: integer
        currency:
          type: string
        decline_code:
          type: string
          nullable: true
        failure_code:
          type: string
          nullable: true
        occurred_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
      nullable: false
    Offer:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - kind
        - name
        - params
        - archived_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - offer
        id:
          type: string
          example: off_8Kd2mX7pQs4LzA1b
        kind:
          type: string
          enum:
            - discount
            - pause
            - downgrade
            - talk
        name:
          type: string
          example: 30% for 3 months
        params:
          type: object
          additionalProperties: true
          example:
            percent: 30
            months: 3
        archived_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    CancelSession:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - step
        - outcome
        - locale
        - holdout
        - published
        - customer
        - subscription
        - subscriptions
        - reasons
        - offer
        - talk_url
        - texts
        - feedback_enabled
        - feedback
        - theme
        - cancel_url_now
        - cancel_status
        - cancel_effective_at
        - result
        - access_until
        - confirmation_email_sent
        - review
        - expires_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - cancel_session
        id:
          type: string
          example: ses_3Ls9dK2mQx7VbN4p
        livemode:
          type: boolean
        step:
          type: string
          enum:
            - choose_subscription
            - reason
            - offer
            - feedback
            - confirm
            - done
        outcome:
          type: string
          nullable: true
          enum:
            - saved
            - cancelled
            - abandoned
            - null
        locale:
          type: string
        holdout:
          type: boolean
        published:
          type: boolean
        customer:
          type: object
          nullable: true
          additionalProperties: true
        subscription:
          type: object
          additionalProperties: false
          required:
            - id
            - plan
            - status
            - amount_cents
            - currency
            - interval
            - interval_count
            - current_period_end
            - cancel_at_period_end
          properties:
            id:
              type: string
              example: nsub_7Hd2kQ9xLm3VbN4p
            plan:
              type: string
              nullable: true
            status:
              type: string
              enum:
                - incomplete
                - incomplete_expired
                - trialing
                - active
                - past_due
                - canceled
                - unpaid
                - paused
            amount_cents:
              type: integer
              description: Price per interval (quantity included), minor unit.
            currency:
              type: string
              nullable: true
            interval:
              type: string
              nullable: true
              example: month
            interval_count:
              type: integer
            current_period_end:
              type: string
              format: date-time
              nullable: true
              description: Next renewal.
            cancel_at_period_end:
              type: boolean
          nullable: true
          description: The subscription this session targets (offers, saved MRR, cancellation). Null at choose_subscription, for an unknown customer, or without a live subscription.
        subscriptions:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - plan
              - status
              - amount_cents
              - currency
              - interval
              - interval_count
              - current_period_end
              - cancel_at_period_end
            properties:
              id:
                type: string
                example: nsub_7Hd2kQ9xLm3VbN4p
              plan:
                type: string
                nullable: true
              status:
                type: string
                enum:
                  - incomplete
                  - incomplete_expired
                  - trialing
                  - active
                  - past_due
                  - canceled
                  - unpaid
                  - paused
              amount_cents:
                type: integer
                description: Price per interval (quantity included), minor unit.
              currency:
                type: string
                nullable: true
              interval:
                type: string
                nullable: true
                example: month
              interval_count:
                type: integer
              current_period_end:
                type: string
                format: date-time
                nullable: true
                description: Next renewal.
              cancel_at_period_end:
                type: boolean
            nullable: false
          description: "At choose_subscription only (several live subscriptions, none named by the token's `subscription` claim): the customer picks one (answer { subscription }) or cancels one (cancel { subscription }). Empty otherwise."
        reasons:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - label
              - free_text
            properties:
              key:
                type: string
                example: too_expensive
              label:
                type: string
              free_text:
                type: boolean
            nullable: false
        offer:
          type: object
          additionalProperties: false
          required:
            - id
            - kind
            - title
            - url
            - percent
            - months
            - plan_name
          properties:
            id:
              type: string
            kind:
              type: string
              enum:
                - discount
                - pause
                - downgrade
                - talk
            title:
              type: string
              description: In the session's language, with the account's text for that offer kind (texts.<lang>.offer_<kind>) when set.
            url:
              type: string
              nullable: true
              description: "talk: the booking link."
            percent:
              type: integer
              nullable: true
              description: "discount: percent off."
            months:
              type: integer
              nullable: true
              description: "discount: how many months; pause: how long."
            plan_name:
              type: string
              nullable: true
              description: "downgrade: the target plan's name, when the offer has one."
          nullable: true
        talk_url:
          type: string
          nullable: true
        texts:
          type: object
          additionalProperties:
            type: string
        feedback_enabled:
          type: boolean
          description: Same as feedback.enabled (kept for older widgets).
        feedback:
          $ref: "#/components/schemas/CancelFeedback"
        theme:
          allOf:
            - $ref: "#/components/schemas/CancelFlowTheme"
          description: "This session's design: the flow's, or its segment's / A/B variant's."
        cancel_url_now:
          type: string
          nullable: true
          description: "Shown once, when the session is created: cancels at any step."
        cancel_status:
          type: string
          nullable: true
          enum:
            - pending
            - done
            - requested
            - failed
            - null
        cancel_effective_at:
          type: string
          format: date-time
          nullable: true
        result:
          allOf:
            - $ref: "#/components/schemas/CancelSessionResult"
          nullable: true
          description: "Once an offer is accepted: outcome saved (discount, pause, downgrade), or a talk offer accepted (the session goes on). Null otherwise."
        access_until:
          type: string
          format: date-time
          nullable: true
          description: "Cancelled: access lasts until then (cancel_effective_at: the period end, or now when immediate). Null otherwise, and while the cancellation is pending."
        confirmation_email_sent:
          type: boolean
          description: "A cancellation confirmation email was actually queued. NoChurn sends none today: always false (the widget hides that fact)."
        review:
          type: object
          additionalProperties: false
          required:
            - platform
            - url
            - text
            - cta
          properties:
            platform:
              type: string
              enum:
                - g2
                - capterra
                - trustpilot
                - google
                - app_store
                - product_hunt
            url:
              type: string
            text:
              type: string
            cta:
              type: string
          nullable: true
          description: Asked on the last step to happy leavers only, once per customer, never tied to an offer.
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
      nullable: false
    CancelFlow:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - published
        - version
        - locale
        - texts
        - reasons
        - feedback_enabled
        - feedback
        - theme
        - languages
        - cancel_fallback_url
      properties:
        object:
          type: string
          enum:
            - cancel_flow
        id:
          type: string
        published:
          type: boolean
        version:
          type: integer
          nullable: true
        locale:
          type: string
        texts:
          type: object
          additionalProperties:
            type: string
        reasons:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - label
              - free_text
            properties:
              key:
                type: string
                example: too_expensive
              label:
                type: string
              free_text:
                type: boolean
            nullable: false
        feedback_enabled:
          type: boolean
        feedback:
          $ref: "#/components/schemas/CancelFeedback"
        theme:
          $ref: "#/components/schemas/CancelFlowTheme"
        languages:
          type: array
          items:
            type: string
        cancel_fallback_url:
          type: string
          nullable: true
          description: "With a publishable key: the fallback cancel link (POST { token } as a form). Null for a secret key."
      nullable: false
    CancelFlowTheme:
      type: object
      additionalProperties: false
      required:
        - object
        - preset
        - format
        - tokens
        - font_url
        - brand
        - logo_url
        - app_name
        - end_screen
        - custom_css
        - cancel_button_check
        - classes
      properties:
        object:
          type: string
          enum:
            - cancel_flow_theme
        preset:
          type: string
          enum:
            - light
            - dark
            - playful
            - custom_css
            - headless
        format:
          type: string
          enum:
            - modal
            - panel
            - page
          description: modal (centred), panel (side panel) or page (full page).
        tokens:
          type: object
          additionalProperties: false
          required:
            - bg
            - surface
            - text
            - muted
            - accent
            - accent_text
            - border
            - danger
            - font
            - font_size
            - radius
            - radius_control
            - overlay
            - max_width
            - shadow
            - spacing
            - border_width
            - border_style
            - font_scale
            - font_weight_title
          properties:
            bg:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            surface:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            text:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            muted:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            accent:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            accent_text:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            border:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            danger:
              type: string
              pattern: ^#[0-9a-fA-F]{6}$
            font:
              type: string
              maxLength: 200
              example: Inter, system-ui, sans-serif
              description: A font stack. Its first family loads from font_url when it is an allowed Google Font.
            font_size:
              type: number
              minimum: 12
              maximum: 20
              description: Base size, px.
            radius:
              type: integer
              minimum: 0
              maximum: 999
              description: Modal corners, px.
            radius_control:
              type: integer
              minimum: 0
              maximum: 999
              description: Buttons and fields, px.
            overlay:
              type: number
              minimum: 0
              maximum: 1
              description: Backdrop opacity.
            max_width:
              type: integer
              minimum: 280
              maximum: 1200
              description: Modal width, px.
            shadow:
              type: string
              enum:
                - none
                - sm
                - md
                - lg
            spacing:
              type: string
              enum:
                - compact
                - regular
                - comfortable
              description: Density.
            border_width:
              type: integer
              minimum: 0
              maximum: 4
              description: px.
            border_style:
              type: string
              enum:
                - solid
                - dashed
                - none
            font_scale:
              type: number
              minimum: 1
              maximum: 2
              description: Title size = font_size × font_scale.
            font_weight_title:
              type: integer
              enum:
                - 400
                - 500
                - 600
                - 700
          nullable: false
          description: The --nc-* custom properties (--nc-accent-text for accent_text). Enumerated values are mapped to CSS by the widget.
        font_url:
          type: string
          nullable: true
          description: Google Fonts stylesheet of the font token's first family (allowed fonts only), else null.
        brand:
          type: string
          enum:
            - logo
            - name
            - none
          description: "What the header shows: logo (logo_url, else app_name), name (app_name) or none (nothing). logo_url and app_name already follow it: null when the header must not show them."
        logo_url:
          type: string
          nullable: true
          description: https URL of the logo (uploaded, or set in the editor) when brand is logo, else null (show app_name).
        app_name:
          type: string
          nullable: true
          description: "The account's name: the text shown in place of a logo; null when brand is none."
        end_screen:
          type: object
          additionalProperties: false
          required:
            - style
            - auto_close_seconds
          properties:
            style:
              type: string
              enum:
                - celebrate
                - glow
                - quiet
              description: "Saved customer: celebrate (default), glow or quiet."
            auto_close_seconds:
              type: integer
              nullable: true
              minimum: 3
              maximum: 30
              description: "Saved screen only: closes by itself after this (default 6); null = never."
          nullable: false
          description: The modal's last step (draft.design.end_screen). A cancelled customer's end screen is always calm, whatever the style.
        custom_css:
          type: string
          description: Cleaned on the server (no @import, url() only to Google Fonts).
        cancel_button_check:
          type: string
          enum:
            - ok
            - fallback_applied
        classes:
          type: array
          items:
            type: string
            enum:
              - nc-modal
              - nc-title
              - nc-reason
              - nc-offer
              - nc-btn-primary
              - nc-btn-cancel
              - nc-footer
      nullable: false
    CancelFlowLogo:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - flow
        - url
        - content_type
        - byte_size
        - width
        - height
        - created_at
      properties:
        object:
          type: string
          enum:
            - cancel_flow_logo
        id:
          type: string
          example: clg_3Ls9dK2mQx7VbN4p
        flow:
          type: string
          example: flw_3Ls9dK2mQx7VbN4p
        url:
          type: string
          example: https://api.nochurn.sh/assets/logos/3Ls9dK2mQx7VbN4pZ8rT1yWq5eHn6uJc.png
          description: "Public and stable: put it in draft.design.logo_url. No account id in it; served without auth."
        content_type:
          type: string
          enum:
            - image/png
            - image/jpeg
            - image/webp
        byte_size:
          type: integer
        width:
          type: integer
          description: px
        height:
          type: integer
          description: px
        created_at:
          type: string
          format: date-time
      nullable: false
    CancelFeedback:
      type: object
      additionalProperties: false
      required:
        - enabled
        - required
      properties:
        enabled:
          type: boolean
          description: The feedback step is shown.
        required:
          type: boolean
          description: '"Continue" needs a text (400 otherwise). Cancelling never does.'
      nullable: false
    WidgetPing:
      type: object
      additionalProperties: false
      required:
        - object
        - detected_at
        - cancel_fallback_url
      properties:
        object:
          type: string
          enum:
            - widget_ping
        detected_at:
          type: string
          format: date-time
        cancel_fallback_url:
          type: string
          nullable: true
          description: "The key's fallback cancel link: POST { token } as a form when a cancel session cannot be opened."
      nullable: false
    WidgetGuardReport:
      type: object
      additionalProperties: false
      required:
        - object
        - recorded
        - event
      properties:
        object:
          type: string
          enum:
            - widget_guard_report
        recorded:
          type: boolean
          description: "false: the same alert was recorded less than an hour ago."
        event:
          type: string
          nullable: true
          description: The Event (nevt_) recorded.
      nullable: false
    ReviewPlatform:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - kind
        - url
        - status
        - review_count
        - rating
        - rules
        - created_at
      properties:
        object:
          type: string
          enum:
            - review_platform
        id:
          type: string
          example: rvp_5Hd2kQ9xLm3VbN7p
        kind:
          type: string
          enum:
            - g2
            - capterra
            - trustpilot
            - google
            - app_store
            - product_hunt
        url:
          type: string
        status:
          type: string
          enum:
            - active
            - disabled
        review_count:
          type: integer
          nullable: true
        rating:
          type: number
          nullable: true
        rules:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
      nullable: false
    ReviewRequestStats:
      type: object
      additionalProperties: false
      required:
        - object
        - period_start
        - cancellations
        - requested
        - clicked
        - follow_ups_sent
        - by_platform
      properties:
        object:
          type: string
          enum:
            - review_request_stats
        period_start:
          type: string
          format: date-time
        cancellations:
          type: integer
        requested:
          type: integer
        clicked:
          type: integer
        follow_ups_sent:
          type: integer
        by_platform:
          type: object
          additionalProperties:
            type: integer
      nullable: false
    SendScheduleFlowOverride:
      type: object
      additionalProperties: false
      required:
        - object
        - flow
        - version
        - published
        - draft
        - segments
        - variants
      properties:
        object:
          type: string
          enum:
            - send_schedule_flow_override
        flow:
          type: object
          additionalProperties: false
          required:
            - id
            - kind
            - name
          properties:
            id:
              type: string
              example: flw_3Ls9dK2mQx7VbN4p
            kind:
              type: string
              enum:
                - prevention
                - recovery
                - cancel
                - winback
            name:
              type: string
          nullable: false
        version:
          type: object
          additionalProperties: false
          required:
            - id
            - number
            - published_to
          properties:
            id:
              type: string
              example: flv_9Kd2mX7pQs4LzA1b
            number:
              type: integer
            published_to:
              type: string
              enum:
                - test
                - live
          nullable: true
          description: The version that runs in this mode; null when none.
        published:
          type: object
          additionalProperties: true
          description: Options the version of this mode sets itself.
          properties:
            audience:
              type: string
              enum:
                - b2b
                - b2c
            respect_customer_timezone:
              type: boolean
            skip_weekends:
              type: boolean
            skip_public_holidays:
              type: boolean
              description: Holidays of the customer's country (and state).
            windows:
              type: object
              properties:
                weekday:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
                weekend:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
            quiet_hours:
              type: object
              properties:
                enabled:
                  type: boolean
                from:
                  type: string
                to:
                  type: string
            days_off:
              type: array
              items:
                type: string
                format: date
        draft:
          type: object
          additionalProperties: true
          description: Options the draft sets itself (published at the next publication).
          properties:
            audience:
              type: string
              enum:
                - b2b
                - b2c
            respect_customer_timezone:
              type: boolean
            skip_weekends:
              type: boolean
            skip_public_holidays:
              type: boolean
              description: Holidays of the customer's country (and state).
            windows:
              type: object
              properties:
                weekday:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
                weekend:
                  type: array
                  items:
                    type: array
                    items:
                      type: string
            quiet_hours:
              type: object
              properties:
                enabled:
                  type: boolean
                from:
                  type: string
                to:
                  type: string
            days_off:
              type: array
              items:
                type: string
                format: date
        segments:
          type: array
          description: "Segments of the flow whose THEN sets options itself. A segment has no mode and no draft: it applies to the next runs of both modes."
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - name
              - is_default
              - overrides
            properties:
              id:
                type: string
                example: seg_4Jd8sK2mQx7VbN4p
              name:
                type: string
              is_default:
                type: boolean
                description: Everyone else.
              overrides:
                type: object
                additionalProperties: true
                description: Options the segment sets itself.
                properties:
                  audience:
                    type: string
                    enum:
                      - b2b
                      - b2c
                  respect_customer_timezone:
                    type: boolean
                  skip_weekends:
                    type: boolean
                  skip_public_holidays:
                    type: boolean
                    description: Holidays of the customer's country (and state).
                  windows:
                    type: object
                    properties:
                      weekday:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                      weekend:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                  quiet_hours:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      from:
                        type: string
                      to:
                        type: string
                  days_off:
                    type: array
                    items:
                      type: string
                      format: date
            nullable: false
        variants:
          type: array
          description: "A/B variants of the flow that set options themselves: every variant of a draft or running test, the winner of a decided test (stopped tests and losing variants never apply again). No mode. Those of a running test are listed with applies false: applying does not change them."
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - label
              - is_control
              - experiment
              - overrides
              - applies
              - not_applied_reason
            properties:
              id:
                type: string
                example: abv_7Hs2dK9mQx4VbN1p
              label:
                type: string
                enum:
                  - A
                  - B
                  - C
                  - D
              is_control:
                type: boolean
              experiment:
                type: object
                additionalProperties: false
                required:
                  - id
                  - name
                  - status
                properties:
                  id:
                    type: string
                    example: abt_2Kd9sL4mQx7VbN3p
                  name:
                    type: string
                  status:
                    type: string
                    enum:
                      - draft
                      - running
                      - decided
                    description: "draft: not started, the variant applies once it starts."
                nullable: false
              overrides:
                type: object
                additionalProperties: true
                description: Options the variant sets itself.
                properties:
                  audience:
                    type: string
                    enum:
                      - b2b
                      - b2c
                  respect_customer_timezone:
                    type: boolean
                  skip_weekends:
                    type: boolean
                  skip_public_holidays:
                    type: boolean
                    description: Holidays of the customer's country (and state).
                  windows:
                    type: object
                    properties:
                      weekday:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                      weekend:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                  quiet_hours:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      from:
                        type: string
                      to:
                        type: string
                  days_off:
                    type: array
                    items:
                      type: string
                      format: date
              applies:
                type: boolean
                description: "false: the variant's test is running; applying leaves it unchanged (stop the test to apply)."
              not_applied_reason:
                type: string
                nullable: true
                enum:
                  - running_test
                  - null
                description: Why applying does not change the variant; null when it does.
            nullable: false
      nullable: false
    SendScheduleFlowsApplied:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - applied
        - counts
        - data
      properties:
        object:
          type: string
          enum:
            - send_schedule_flows_applied
        livemode:
          type: boolean
        applied:
          type: integer
          description: Number of flows changed, in their own settings, segments or A/B variants (0 when none overrode the account).
        counts:
          type: object
          additionalProperties: false
          required:
            - flows
            - segments
            - variants
          properties:
            flows:
              type: integer
              description: Flows whose own settings changed (published version of this mode or draft).
            segments:
              type: integer
            variants:
              type: integer
          nullable: false
        data:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - object
              - flow
              - version
              - published
              - draft
              - segments
              - variants
            properties:
              object:
                type: string
                enum:
                  - send_schedule_flow_applied
              flow:
                type: object
                additionalProperties: false
                required:
                  - id
                  - kind
                  - name
                properties:
                  id:
                    type: string
                    example: flw_3Ls9dK2mQx7VbN4p
                  kind:
                    type: string
                    enum:
                      - prevention
                      - recovery
                      - cancel
                      - winback
                  name:
                    type: string
                nullable: false
              version:
                type: object
                additionalProperties: false
                required:
                  - id
                  - number
                  - published_to
                properties:
                  id:
                    type: string
                    example: flv_9Kd2mX7pQs4LzA1b
                  number:
                    type: integer
                  published_to:
                    type: string
                    enum:
                      - test
                      - live
                nullable: true
                description: The version published with those options at null; null when only the draft changed.
              published:
                type: object
                additionalProperties: true
                description: The options the replaced version set (now null).
                properties:
                  audience:
                    type: string
                    enum:
                      - b2b
                      - b2c
                  respect_customer_timezone:
                    type: boolean
                  skip_weekends:
                    type: boolean
                  skip_public_holidays:
                    type: boolean
                    description: Holidays of the customer's country (and state).
                  windows:
                    type: object
                    properties:
                      weekday:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                      weekend:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                  quiet_hours:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      from:
                        type: string
                      to:
                        type: string
                  days_off:
                    type: array
                    items:
                      type: string
                      format: date
              draft:
                type: object
                additionalProperties: true
                description: The options the draft set (now null).
                properties:
                  audience:
                    type: string
                    enum:
                      - b2b
                      - b2c
                  respect_customer_timezone:
                    type: boolean
                  skip_weekends:
                    type: boolean
                  skip_public_holidays:
                    type: boolean
                    description: Holidays of the customer's country (and state).
                  windows:
                    type: object
                    properties:
                      weekday:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                      weekend:
                        type: array
                        items:
                          type: array
                          items:
                            type: string
                  quiet_hours:
                    type: object
                    properties:
                      enabled:
                        type: boolean
                      from:
                        type: string
                      to:
                        type: string
                  days_off:
                    type: array
                    items:
                      type: string
                      format: date
              segments:
                type: array
                description: The segments whose THEN no longer sets those options.
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - id
                    - name
                    - is_default
                    - overrides
                  properties:
                    id:
                      type: string
                      example: seg_4Jd8sK2mQx7VbN4p
                    name:
                      type: string
                    is_default:
                      type: boolean
                      description: Everyone else.
                    overrides:
                      type: object
                      additionalProperties: true
                      description: "The options the segment set (now removed: it follows its flow)."
                      properties:
                        audience:
                          type: string
                          enum:
                            - b2b
                            - b2c
                        respect_customer_timezone:
                          type: boolean
                        skip_weekends:
                          type: boolean
                        skip_public_holidays:
                          type: boolean
                          description: Holidays of the customer's country (and state).
                        windows:
                          type: object
                          properties:
                            weekday:
                              type: array
                              items:
                                type: array
                                items:
                                  type: string
                            weekend:
                              type: array
                              items:
                                type: array
                                items:
                                  type: string
                        quiet_hours:
                          type: object
                          properties:
                            enabled:
                              type: boolean
                            from:
                              type: string
                            to:
                              type: string
                        days_off:
                          type: array
                          items:
                            type: string
                            format: date
                  nullable: false
              variants:
                type: array
                description: The A/B variants that no longer set those options (never those of a running test).
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - id
                    - label
                    - is_control
                    - experiment
                    - overrides
                    - applies
                    - not_applied_reason
                  properties:
                    id:
                      type: string
                      example: abv_7Hs2dK9mQx4VbN1p
                    label:
                      type: string
                      enum:
                        - A
                        - B
                        - C
                        - D
                    is_control:
                      type: boolean
                    experiment:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - status
                      properties:
                        id:
                          type: string
                          example: abt_2Kd9sL4mQx7VbN3p
                        name:
                          type: string
                        status:
                          type: string
                          enum:
                            - draft
                            - running
                            - decided
                          description: "draft: not started, the variant applies once it starts."
                      nullable: false
                    overrides:
                      type: object
                      additionalProperties: true
                      description: "The options the variant set (now removed: it follows its flow)."
                      properties:
                        audience:
                          type: string
                          enum:
                            - b2b
                            - b2c
                        respect_customer_timezone:
                          type: boolean
                        skip_weekends:
                          type: boolean
                        skip_public_holidays:
                          type: boolean
                          description: Holidays of the customer's country (and state).
                        windows:
                          type: object
                          properties:
                            weekday:
                              type: array
                              items:
                                type: array
                                items:
                                  type: string
                            weekend:
                              type: array
                              items:
                                type: array
                                items:
                                  type: string
                        quiet_hours:
                          type: object
                          properties:
                            enabled:
                              type: boolean
                            from:
                              type: string
                            to:
                              type: string
                        days_off:
                          type: array
                          items:
                            type: string
                            format: date
                    applies:
                      type: boolean
                      description: "false: the variant's test is running; applying leaves it unchanged (stop the test to apply)."
                    not_applied_reason:
                      type: string
                      nullable: true
                      enum:
                        - running_test
                        - null
                      description: Why applying does not change the variant; null when it does.
                  nullable: false
            nullable: false
      nullable: false
    ChurnScore:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - score
        - level
        - model_version
        - computed_at
        - factors
        - flag
        - history
      properties:
        object:
          type: string
          enum:
            - churn_score
        customer:
          type: string
          example: cst_4fJ8sK2pQ9wLm3Xz
        score:
          type: integer
          nullable: true
          minimum: 0
          maximum: 100
          description: null until computed.
        level:
          type: string
          enum:
            - low
            - medium
            - high
            - null
          nullable: true
          description: Thresholds of the prevention flow (high ≥ 70, medium ≥ 40 by default).
        model_version:
          type: string
          nullable: true
          example: heuristic-v1
        computed_at:
          type: string
          format: date-time
          nullable: true
        factors:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - label
              - points
            properties:
              key:
                type: string
                example: payment_method
              label:
                type: string
                example: Prepaid card
              points:
                type: integer
                example: 65
            nullable: false
          description: What explains the score, points added (or removed).
        flag:
          $ref: "#/components/schemas/CustomerFlag"
        history:
          type: array
          description: Last 30 computations, newest first.
          items:
            type: object
            additionalProperties: false
            required:
              - score
              - level
              - trigger
              - computed_at
            properties:
              score:
                type: integer
              level:
                type: string
                enum:
                  - low
                  - medium
                  - high
              trigger:
                type: string
                enum:
                  - signup
                  - daily
                  - signal
                  - sync
                  - manual
                  - attributes
                  - cancel_session
                  - import
              computed_at:
                type: string
                format: date-time
            nullable: false
      nullable: false
    CustomerFlag:
      type: object
      additionalProperties: false
      required:
        - reason
        - flagged_at
      properties:
        reason:
          type: string
          enum:
            - card_removed
            - card_expiring
        flagged_at:
          type: string
          format: date-time
      nullable: true
      description: "FLAGGED: a prevention signal before the renewal. Cleared when a new card arrives."
    AttributeDefinition:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - key
        - type
        - sensitive
        - customers_count
        - coverage
        - last_seen_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - attribute_definition
        id:
          type: string
          example: atd_7Hq2LmX9pK3sNd4v
        key:
          type: string
          example: signup_source
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - date
          description: Inferred at the first send, then locked.
        sensitive:
          type: boolean
          description: Never sent to the AI.
        customers_count:
          type: integer
          description: Customers of this mode with a value.
        coverage:
          type: number
          nullable: true
          description: customers_count / customers of this mode; null without customers.
        last_seen_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    Import:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - status
        - filename
        - mapping
        - rows_total
        - rows_imported
        - rows_failed
        - errors
        - livemode
        - created_at
        - started_at
        - finished_at
      properties:
        object:
          type: string
          enum:
            - import
        id:
          type: string
          example: imp_3Kd9sL2mQx7VbN4p
        status:
          type: string
          enum:
            - pending
            - running
            - completed
            - failed
        filename:
          type: string
          nullable: true
          example: customers.csv
        mapping:
          type: object
          additionalProperties:
            type: string
          example:
            User ID: external_id
            Plan: attribute:plan
        rows_total:
          type: integer
        rows_imported:
          type: integer
        rows_failed:
          type: integer
        errors:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - line
              - code
              - message
            properties:
              line:
                type: integer
                description: Line in the file (1 = header).
              code:
                type: string
              message:
                type: string
            nullable: false
          description: Row by row, 500 at most.
        livemode:
          type: boolean
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        finished_at:
          type: string
          format: date-time
          nullable: true
      nullable: false
    ImportPreview:
      type: object
      additionalProperties: false
      required:
        - object
        - columns
        - rows_total
        - rows
      properties:
        object:
          type: string
          enum:
            - import_preview
        columns:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - name
              - target
            properties:
              name:
                type: string
              target:
                type: string
                example: attribute:plan
            nullable: false
        rows_total:
          type: integer
        rows:
          type: array
          items:
            type: object
            additionalProperties:
              type: string
              nullable: true
          description: First 10 rows.
      nullable: false
    MessageReply:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - message
        - customer
        - customer_email
        - customer_name
        - flow_run
        - flow
        - from
        - subject
        - text
        - category
        - summary
        - needs_human
        - status
        - handled_at
        - answer_message
        - received_at
        - livemode
      properties:
        object:
          type: string
          enum:
            - message_reply
        id:
          type: string
          example: rpl_8Kq2mX7pLs4NzA1b
        message:
          type: string
          description: The email it answers (msg_…).
        customer:
          type: string
          nullable: true
        customer_email:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
        flow_run:
          type: string
          nullable: true
        flow:
          type: string
          nullable: true
          enum:
            - prevention
            - recovery
            - cancel
            - winback
            - null
        from:
          type: string
        subject:
          type: string
          nullable: true
        text:
          type: string
          description: "The reply without quoted history. Customer content: show as text."
        category:
          type: string
          enum:
            - wants_to_return
            - question
            - not_interested
            - unsubscribe
            - out_of_office
            - other
        summary:
          type: string
          nullable: true
          description: One line by the AI; null when keyword rules sorted it.
        needs_human:
          type: boolean
        status:
          type: string
          enum:
            - open
            - handled
            - ignored
        handled_at:
          type: string
          format: date-time
          nullable: true
        answer_message:
          type: string
          nullable: true
          description: The answer sent from To do (msg_…).
        received_at:
          type: string
          format: date-time
        livemode:
          type: boolean
      nullable: false
    MessageReplyPage:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
        - open_count
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/MessageReply"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
        open_count:
          type: integer
          description: Replies still open in this mode (To do badge).
      nullable: false
    FlowStats:
      type: object
      additionalProperties: false
      required:
        - object
        - flow
        - kind
        - livemode
        - period
        - kpis
        - funnel
        - holdout
      properties:
        object:
          type: string
          enum:
            - flow_stats
        flow:
          type: string
        kind:
          type: string
          enum:
            - prevention
            - recovery
            - cancel
            - winback
        livemode:
          type: boolean
        period:
          type: object
          additionalProperties: false
          required:
            - key
            - from
            - to
          properties:
            key:
              type: string
              enum:
                - month
                - 30d
                - 90d
                - 12m
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
          nullable: false
        kpis:
          type: array
          items:
            type: object
            required:
              - key
              - label
              - value
              - unit
              - sample_size
            additionalProperties: false
            properties:
              key:
                type: string
                example: recovery_rate
              label:
                type: string
              value:
                type: number
                nullable: true
              unit:
                type: string
                enum:
                  - count
                  - money
                  - rate
                  - days
              sample_size:
                type: integer
              pending:
                type: integer
                description: Outcomes reached but not confirmed yet (30 days).
              currency:
                type: string
              open_rate:
                type: number
                nullable: true
              breakdown:
                type: array
                description: "Per original currency: the amount and its value in the reporting currency (ECB rate of its day)."
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - currency
                    - amount_cents
                    - converted_cents
                    - unconverted_cents
                  properties:
                    currency:
                      type: string
                      example: usd
                    amount_cents:
                      type: integer
                      description: In this currency.
                    converted_cents:
                      type: integer
                      description: Its converted part, in the reporting currency.
                    unconverted_cents:
                      type: integer
                      description: "In this currency: amounts of days without an ECB rate, left out of the total."
                  nullable: false
          description: 4 KPIs; saved / recovered / won back / prevented count confirmed outcomes.
        funnel:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - label
              - count
              - share
            properties:
              key:
                type: string
              label:
                type: string
              count:
                type: integer
              share:
                type: number
                nullable: true
            nullable: false
          description: "5 steps: runs started in the period that reached each step, share of the first."
        holdout:
          type: object
          additionalProperties: false
          required:
            - percent
            - runs
            - natural_rate
            - treated_rate
            - lift
            - sample_size
          properties:
            percent:
              type: integer
              nullable: true
            runs:
              type: integer
            natural_rate:
              type: number
              nullable: true
              description: What happened without NoChurn (holdout group).
            treated_rate:
              type: number
              nullable: true
            lift:
              type: number
              nullable: true
            sample_size:
              type: integer
          nullable: false
      nullable: false
    Segment:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - flow
        - name
        - position
        - is_default
        - conditions
        - overrides
        - customers_count
        - mrr_cents
        - counted_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - segment
        id:
          type: string
          example: seg_2Kd9sL2mQx7VbN4p
        flow:
          type: string
        name:
          type: string
        position:
          type: integer
        is_default:
          type: boolean
          description: "Everyone else: always last, no conditions."
        conditions:
          type: object
          description: "{ any: [ { all: [ { source, field, op, value } ] } ] }"
        overrides:
          type: object
          description: "THEN: patch of the flow config, frozen on each run at entry. A send_window option left out follows the flow."
        customers_count:
          type: integer
          nullable: true
        mrr_cents:
          type: integer
          nullable: true
        counted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    SegmentField:
      type: object
      additionalProperties: false
      required:
        - source
        - field
        - type
        - label
        - ops
      properties:
        source:
          type: string
          enum:
            - stripe
            - nochurn
            - custom
        field:
          type: string
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - date
        label:
          type: string
        ops:
          type: array
          items:
            type: string
      nullable: false
    SegmentPreview:
      type: object
      additionalProperties: false
      required:
        - object
        - customers_count
        - mrr_cents
        - counted_at
      properties:
        object:
          type: string
          enum:
            - segment_preview
        customers_count:
          type: integer
        mrr_cents:
          type: integer
        counted_at:
          type: string
          format: date-time
      nullable: false
    Experiment:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - flow
        - segment
        - name
        - target
        - goal_metric
        - status
        - auto_pick
        - threshold
        - min_sample
        - started_at
        - stopped_at
        - winner
        - stats_at
        - created_at
        - variants
      properties:
        object:
          type: string
          enum:
            - experiment
        id:
          type: string
          example: abt_5Hq2LmX9pK3sNd4v
        flow:
          type: string
        segment:
          type: string
          nullable: true
        name:
          type: string
        target:
          type: string
          enum:
            - offer
            - text
            - timing
            - design
            - channel
        goal_metric:
          type: string
          enum:
            - saved
            - card_updated_before_renewal
            - recovered_within_7d
            - won_back
        status:
          type: string
          enum:
            - draft
            - running
            - stopped
            - decided
        auto_pick:
          type: boolean
        threshold:
          type: number
        min_sample:
          type: integer
        started_at:
          type: string
          format: date-time
          nullable: true
        stopped_at:
          type: string
          format: date-time
          nullable: true
        winner:
          type: string
          nullable: true
        stats_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        variants:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - label
              - is_control
              - weight
              - overrides
              - exposures
              - conversions
              - conversion_rate
              - probability_best
            properties:
              id:
                type: string
              label:
                type: string
                enum:
                  - A
                  - B
                  - C
                  - D
              is_control:
                type: boolean
              weight:
                type: integer
              overrides:
                type: object
                description: Patch of the flow config (on top of the segment's). A send_window option left out follows the flow.
              exposures:
                type: integer
              conversions:
                type: integer
              conversion_rate:
                type: number
                nullable: true
              probability_best:
                type: number
                nullable: true
            nullable: false
      nullable: false
    UsageEventBatch:
      type: object
      additionalProperties: false
      required:
        - object
        - accepted
        - inserted
        - duplicates
        - errors
      properties:
        object:
          type: string
          enum:
            - usage_event_batch
        accepted:
          type: integer
        inserted:
          type: integer
        duplicates:
          type: integer
          description: Already received (same idempotency_key).
        errors:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - index
              - code
              - message
            properties:
              index:
                type: integer
              code:
                type: string
              message:
                type: string
            nullable: false
      nullable: false
    WidgetEventBatch:
      type: object
      additionalProperties: false
      required:
        - object
        - accepted
        - duplicates
        - rejected
      properties:
        object:
          type: string
          enum:
            - widget_event_batch
        accepted:
          type: integer
          description: Stored or already received.
        duplicates:
          type: integer
          description: Already received (same idempotency_key in this mode).
        rejected:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - index
              - code
              - message
            properties:
              index:
                type: integer
              code:
                type: string
              message:
                type: string
            nullable: false
      nullable: false
    Insights:
      type: object
      additionalProperties: false
      required:
        - object
        - run
        - insight
      properties:
        object:
          type: string
          enum:
            - insights
        run:
          type: object
          additionalProperties: false
          required:
            - id
            - computed_at
            - population
            - churned
            - baseline_rate
            - window_days
            - drivers
          properties:
            id:
              type: string
            computed_at:
              type: string
              format: date-time
            population:
              type: integer
            churned:
              type: integer
            baseline_rate:
              type: number
              nullable: true
            window_days:
              type: integer
            drivers:
              type: array
              items:
                type: object
                additionalProperties: false
                required:
                  - id
                  - attribute
                  - source
                  - bucket
                  - customers
                  - churned
                  - churn_rate
                  - relative_risk
                  - ci_low
                  - ci_high
                  - q_value
                  - significant
                  - sample_ok
                  - delta_vs_previous
                  - mrr_cents
                properties:
                  id:
                    type: string
                    example: drv_…
                  attribute:
                    type: string
                  source:
                    type: string
                    enum:
                      - nochurn
                      - custom
                  bucket:
                    type: string
                  customers:
                    type: integer
                  churned:
                    type: integer
                  churn_rate:
                    type: number
                  relative_risk:
                    type: number
                    nullable: true
                  ci_low:
                    type: number
                    nullable: true
                  ci_high:
                    type: number
                    nullable: true
                  q_value:
                    type: number
                    nullable: true
                  significant:
                    type: boolean
                  sample_ok:
                    type: boolean
                  delta_vs_previous:
                    type: number
                    nullable: true
                  mrr_cents:
                    type: integer
                    description: MRR of the active customers of the bucket.
                nullable: false
          nullable: true
        insight:
          type: object
          additionalProperties: false
          required:
            - id
            - run
            - created_at
            - model
            - prompt_version
            - headline
            - insights
            - data_gaps
            - drivers
          properties:
            id:
              type: string
              example: ins_…
            run:
              type: string
            created_at:
              type: string
              format: date-time
            model:
              type: string
              nullable: true
            prompt_version:
              type: string
            headline:
              type: string
              nullable: true
            insights:
              type: array
              items:
                type: object
                additionalProperties: true
            data_gaps:
              type: array
              items:
                type: string
            drivers:
              type: array
              items:
                type: object
                additionalProperties: false
                required:
                  - id
                  - attribute
                  - bucket
                  - customers
                  - churn_rate
                  - relative_risk
                  - significant
                properties:
                  id:
                    type: string
                  attribute:
                    type: string
                  bucket:
                    type: string
                  customers:
                    type: integer
                  churn_rate:
                    type: number
                  relative_risk:
                    type: number
                    nullable: true
                  significant:
                    type: boolean
                nullable: false
              description: The drivers the insight cites, from its own run (may be older than `run`).
          nullable: true
      nullable: false
    InsightsRefresh:
      type: object
      additionalProperties: false
      required:
        - object
        - queued
      properties:
        object:
          type: string
          enum:
            - insights_refresh
        queued:
          type: boolean
      nullable: false
    CampaignStats:
      type: object
      additionalProperties: false
      required:
        - object
        - group_by
        - period
        - customers
        - churned
        - churn_rate
        - tracked
        - best
        - worst
        - min_sample
        - rows
      properties:
        object:
          type: string
          enum:
            - campaign_stats
        group_by:
          type: string
        period:
          type: string
        customers:
          type: integer
        churned:
          type: integer
        churn_rate:
          type: number
          nullable: true
        tracked:
          type: integer
        best:
          type: object
          additionalProperties: false
          required:
            - value
            - relative_risk
          properties:
            value:
              type: string
            relative_risk:
              type: number
          nullable: true
        worst:
          type: object
          additionalProperties: false
          required:
            - value
            - relative_risk
          properties:
            value:
              type: string
            relative_risk:
              type: number
          nullable: true
        min_sample:
          type: integer
        rows:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - value
              - customers
              - churned
              - churn_rate
              - relative_risk
              - avg_ltv_cents
              - avg_score
              - mrr_lost_cents
              - sample_ok
            properties:
              value:
                type: string
              customers:
                type: integer
              churned:
                type: integer
              churn_rate:
                type: number
              relative_risk:
                type: number
                nullable: true
              avg_ltv_cents:
                type: integer
              avg_score:
                type: number
                nullable: true
              mrr_lost_cents:
                type: integer
              sample_ok:
                type: boolean
            nullable: false
      nullable: false
    CancelSessionReport:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - step
        - outcome
        - locale
        - holdout
        - published
        - customer
        - subscription
        - subscriptions
        - reasons
        - offer
        - talk_url
        - texts
        - feedback_enabled
        - feedback
        - theme
        - cancel_url_now
        - cancel_status
        - cancel_effective_at
        - result
        - access_until
        - confirmation_email_sent
        - review
        - expires_at
        - created_at
        - reason
        - comment
      properties:
        object:
          type: string
          enum:
            - cancel_session
        id:
          type: string
          example: ses_3Ls9dK2mQx7VbN4p
        livemode:
          type: boolean
        step:
          type: string
          enum:
            - choose_subscription
            - reason
            - offer
            - feedback
            - confirm
            - done
        outcome:
          type: string
          nullable: true
          enum:
            - saved
            - cancelled
            - abandoned
            - null
        locale:
          type: string
        holdout:
          type: boolean
        published:
          type: boolean
        customer:
          type: object
          nullable: true
          additionalProperties: true
        subscription:
          type: object
          additionalProperties: false
          required:
            - id
            - plan
            - status
            - amount_cents
            - currency
            - interval
            - interval_count
            - current_period_end
            - cancel_at_period_end
          properties:
            id:
              type: string
              example: nsub_7Hd2kQ9xLm3VbN4p
            plan:
              type: string
              nullable: true
            status:
              type: string
              enum:
                - incomplete
                - incomplete_expired
                - trialing
                - active
                - past_due
                - canceled
                - unpaid
                - paused
            amount_cents:
              type: integer
              description: Price per interval (quantity included), minor unit.
            currency:
              type: string
              nullable: true
            interval:
              type: string
              nullable: true
              example: month
            interval_count:
              type: integer
            current_period_end:
              type: string
              format: date-time
              nullable: true
              description: Next renewal.
            cancel_at_period_end:
              type: boolean
          nullable: true
          description: The subscription this session targets (offers, saved MRR, cancellation). Null at choose_subscription, for an unknown customer, or without a live subscription.
        subscriptions:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - plan
              - status
              - amount_cents
              - currency
              - interval
              - interval_count
              - current_period_end
              - cancel_at_period_end
            properties:
              id:
                type: string
                example: nsub_7Hd2kQ9xLm3VbN4p
              plan:
                type: string
                nullable: true
              status:
                type: string
                enum:
                  - incomplete
                  - incomplete_expired
                  - trialing
                  - active
                  - past_due
                  - canceled
                  - unpaid
                  - paused
              amount_cents:
                type: integer
                description: Price per interval (quantity included), minor unit.
              currency:
                type: string
                nullable: true
              interval:
                type: string
                nullable: true
                example: month
              interval_count:
                type: integer
              current_period_end:
                type: string
                format: date-time
                nullable: true
                description: Next renewal.
              cancel_at_period_end:
                type: boolean
            nullable: false
          description: "At choose_subscription only (several live subscriptions, none named by the token's `subscription` claim): the customer picks one (answer { subscription }) or cancels one (cancel { subscription }). Empty otherwise."
        reasons:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - label
              - free_text
            properties:
              key:
                type: string
                example: too_expensive
              label:
                type: string
              free_text:
                type: boolean
            nullable: false
        offer:
          type: object
          additionalProperties: false
          required:
            - id
            - kind
            - title
            - url
            - percent
            - months
            - plan_name
          properties:
            id:
              type: string
            kind:
              type: string
              enum:
                - discount
                - pause
                - downgrade
                - talk
            title:
              type: string
              description: In the session's language, with the account's text for that offer kind (texts.<lang>.offer_<kind>) when set.
            url:
              type: string
              nullable: true
              description: "talk: the booking link."
            percent:
              type: integer
              nullable: true
              description: "discount: percent off."
            months:
              type: integer
              nullable: true
              description: "discount: how many months; pause: how long."
            plan_name:
              type: string
              nullable: true
              description: "downgrade: the target plan's name, when the offer has one."
          nullable: true
        talk_url:
          type: string
          nullable: true
        texts:
          type: object
          additionalProperties:
            type: string
        feedback_enabled:
          type: boolean
          description: Same as feedback.enabled (kept for older widgets).
        feedback:
          $ref: "#/components/schemas/CancelFeedback"
        theme:
          allOf:
            - $ref: "#/components/schemas/CancelFlowTheme"
          description: "This session's design: the flow's, or its segment's / A/B variant's."
        cancel_url_now:
          type: string
          nullable: true
          description: "Shown once, when the session is created: cancels at any step."
        cancel_status:
          type: string
          nullable: true
          enum:
            - pending
            - done
            - requested
            - failed
            - null
        cancel_effective_at:
          type: string
          format: date-time
          nullable: true
        result:
          allOf:
            - $ref: "#/components/schemas/CancelSessionResult"
          nullable: true
          description: "Once an offer is accepted: outcome saved (discount, pause, downgrade), or a talk offer accepted (the session goes on). Null otherwise."
        access_until:
          type: string
          format: date-time
          nullable: true
          description: "Cancelled: access lasts until then (cancel_effective_at: the period end, or now when immediate). Null otherwise, and while the cancellation is pending."
        confirmation_email_sent:
          type: boolean
          description: "A cancellation confirmation email was actually queued. NoChurn sends none today: always false (the widget hides that fact)."
        review:
          type: object
          additionalProperties: false
          required:
            - platform
            - url
            - text
            - cta
          properties:
            platform:
              type: string
              enum:
                - g2
                - capterra
                - trustpilot
                - google
                - app_store
                - product_hunt
            url:
              type: string
            text:
              type: string
            cta:
              type: string
          nullable: true
          description: Asked on the last step to happy leavers only, once per customer, never tied to an offer.
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        reason:
          type: string
          nullable: true
        comment:
          type: string
          nullable: true
          description: "Customer text: show as text."
      nullable: false
    CancelSessionReplay:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - steps
        - reason
        - comment
        - feedback
        - outcome
        - cancel_status
      properties:
        object:
          type: string
          enum:
            - cancel_session_replay
        id:
          type: string
        steps:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Timestamped steps.
        reason:
          type: string
          nullable: true
        comment:
          type: string
          nullable: true
        feedback:
          type: string
          nullable: true
        outcome:
          type: string
          nullable: true
          enum:
            - saved
            - cancelled
            - abandoned
            - null
        cancel_status:
          type: string
          nullable: true
          enum:
            - pending
            - done
            - requested
            - failed
            - null
      nullable: false
    Integration:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - kind
        - status
        - external_account
        - config
        - last_synced_at
        - error
        - conversations_count
        - created_at
      properties:
        object:
          type: string
          enum:
            - integration
        id:
          type: string
          example: itg_…
        kind:
          type: string
          enum:
            - intercom
            - crisp
            - zendesk
            - hubspot
            - helpscout
            - gorgias
            - slack
        status:
          type: string
          enum:
            - active
            - error
            - revoked
        external_account:
          type: string
          nullable: true
        config:
          type: object
          properties:
            channel:
              type: string
          description: "Slack: the channel NoChurn posts to."
        last_synced_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
          description: "Actionable: why it needs a reconnection."
        conversations_count:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    IntegrationAuthorizeUrl:
      type: object
      additionalProperties: false
      required:
        - object
        - kind
        - url
      properties:
        object:
          type: string
          enum:
            - authorize_url
        kind:
          type: string
        url:
          type: string
      nullable: false
    Feedback:
      type: object
      additionalProperties: false
      required:
        - object
        - answers_90d
        - run
        - items
      properties:
        object:
          type: string
          enum:
            - feedback
        answers_90d:
          type: integer
        run:
          type: object
          additionalProperties: false
          required:
            - id
            - computed_at
            - summary
            - answers_count
            - currency
            - themes
          properties:
            id:
              type: string
            computed_at:
              type: string
              format: date-time
            summary:
              type: string
              nullable: true
            answers_count:
              type: integer
            currency:
              type: string
              nullable: true
              description: Currency of the themes' mrr_lost_cents (the reporting currency when the run was computed).
            themes:
              type: array
              items:
                type: object
                additionalProperties: false
                required:
                  - id
                  - title
                  - mentions
                  - delta_pct
                  - main_reason_key
                  - mrr_lost_cents
                  - unconverted_currencies
                  - quotes
                properties:
                  id:
                    type: string
                  title:
                    type: string
                  mentions:
                    type: integer
                    description: Counted in code.
                  delta_pct:
                    type: number
                    nullable: true
                  main_reason_key:
                    type: string
                    nullable: true
                  mrr_lost_cents:
                    type: integer
                  unconverted_currencies:
                    type: array
                    items:
                      type: string
                    description: "Currencies of lost MRR without an ECB rate: left out of mrr_lost_cents."
                  quotes:
                    type: array
                    items:
                      type: string
                    description: Feedback item ids to quote.
                nullable: false
          nullable: true
        items:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - source
              - text
              - reason_key
              - theme
              - plan
              - country
              - customer
              - occurred_at
            properties:
              id:
                type: string
              source:
                type: string
                enum:
                  - cancel_session
                  - winback_reply
                  - support
              text:
                type: string
                description: "Customer text: show as text."
              reason_key:
                type: string
                nullable: true
              theme:
                type: string
                nullable: true
              plan:
                type: string
                nullable: true
              country:
                type: string
                nullable: true
              customer:
                type: string
                nullable: true
              occurred_at:
                type: string
                format: date-time
            nullable: false
      nullable: false
    FeedbackRefresh:
      type: object
      additionalProperties: false
      required:
        - object
        - queued
      properties:
        object:
          type: string
          enum:
            - feedback_refresh
        queued:
          type: boolean
      nullable: false
    CustomerInsight:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - summary
        - signals
        - next_step
        - sources
        - computed_at
      properties:
        object:
          type: string
          enum:
            - customer_insight
        id:
          type: string
          example: cai_…
        summary:
          type: string
        signals:
          type: array
          items:
            type: string
        next_step:
          type: string
          nullable: true
        sources:
          type: array
          items:
            type: string
            enum:
              - stripe
              - custom_data
              - support
        computed_at:
          type: string
          format: date-time
      nullable: true
    QuerySource:
      type: object
      additionalProperties: false
      required:
        - source
        - label
        - description
        - fields
      properties:
        source:
          type: string
          enum:
            - customers
            - subscriptions
            - invoices
            - payments
            - cancel_sessions
            - flow_runs
            - usage_events
            - messages
        label:
          type: string
        description:
          type: string
          description: "One line: what a row of this source is."
        fields:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - field
              - type
              - label
              - description
            properties:
              field:
                type: string
                example: plan
              type:
                type: string
                enum:
                  - string
                  - number
                  - boolean
                  - date
              label:
                type: string
              description:
                type: string
                description: One line, shown as a hint.
              values:
                type: array
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - value
                    - label
                  properties:
                    value:
                      type: string
                      example: churned
                    label:
                      type: string
                      example: Churned
                  nullable: false
                maxItems: 50
                description: "Enum-like fields only. closed: every allowed value. observed: the values seen for this account and mode, most frequent first (50 at most, cached 10 minutes): other values are still accepted."
              values_kind:
                type: string
                enum:
                  - closed
                  - observed
                description: Present with values.
            nullable: false
      nullable: false
    QuerySpec:
      type: object
      required:
        - source
      description: NoChurn query builder (no SQL). Either metrics (+ dimensions) or fields.
      properties:
        source:
          type: string
          enum:
            - customers
            - subscriptions
            - invoices
            - payments
            - cancel_sessions
            - flow_runs
            - usage_events
            - messages
        metrics:
          type: array
          maxItems: 5
          items:
            type: object
            required:
              - fn
            properties:
              fn:
                type: string
                enum:
                  - count
                  - count_distinct
                  - sum
                  - avg
                  - min
                  - max
              field:
                type: string
        dimensions:
          type: array
          maxItems: 3
          items:
            type: object
            required:
              - field
            properties:
              field:
                type: string
              bucket:
                type: string
                enum:
                  - day
                  - week
                  - month
                  - quarter
                  - year
        fields:
          type: array
          maxItems: 20
          items:
            type: string
        filters:
          type: array
          maxItems: 10
          items:
            type: object
            required:
              - field
              - op
            properties:
              field:
                type: string
              op:
                type: string
              value: {}
        period:
          type: object
          nullable: true
          properties:
            field:
              type: string
            last_days:
              type: integer
              minimum: 1
              maximum: 730
        sort:
          type: object
          nullable: true
          properties:
            by:
              type: string
              description: metric_0…, a dimension or a field.
            dir:
              type: string
              enum:
                - asc
                - desc
        limit:
          type: integer
          minimum: 1
          maximum: 1000
    QueryRun:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - status
        - spec
        - columns
        - rows
        - row_count
        - duration_ms
        - created_at
      properties:
        object:
          type: string
          enum:
            - query_run
        id:
          type: string
          example: qrn_…
        status:
          type: string
          enum:
            - succeeded
            - failed
            - timeout
        spec:
          $ref: "#/components/schemas/QuerySpec"
        columns:
          type: array
          items:
            type: string
        rows:
          type: array
          items:
            type: array
            items: {}
        row_count:
          type: integer
        duration_ms:
          type: integer
        created_at:
          type: string
          format: date-time
      nullable: false
    QueryTranslation:
      type: object
      additionalProperties: false
      required:
        - object
        - question
        - spec
        - explanation
      properties:
        object:
          type: string
          enum:
            - query_translation
        question:
          type: string
        spec:
          $ref: "#/components/schemas/QuerySpec"
        explanation:
          type: string
      nullable: false
    SavedQuery:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - name
        - question
        - spec
        - view
        - chart
        - created_at
        - schedule
      properties:
        object:
          type: string
          enum:
            - saved_query
        id:
          type: string
          example: qry_…
        name:
          type: string
        question:
          type: string
          nullable: true
        spec:
          $ref: "#/components/schemas/QuerySpec"
        view:
          type: string
          enum:
            - table
            - chart
        chart:
          type: object
        created_at:
          type: string
          format: date-time
        schedule:
          type: object
          additionalProperties: false
          required:
            - cadence
            - day
            - time
            - timezone
            - destination
            - target
            - next_run_at
            - last_run
          properties:
            cadence:
              type: string
              enum:
                - daily
                - weekly
                - monthly
            day:
              oneOf:
                - type: string
                  enum:
                    - monday
                    - tuesday
                    - wednesday
                    - thursday
                    - friday
                    - saturday
                    - sunday
                - type: integer
                  minimum: 1
                  maximum: 28
              nullable: true
              description: "weekly: the weekday; monthly: the day of the month (1–28); daily: null."
            time:
              type: string
              example: 08:00
              description: "Read only: the hour the report goes out (the hour it was first scheduled at), in timezone."
            timezone:
              type: string
              example: Europe/Paris
              description: The account's time zone.
            destination:
              type: string
              enum:
                - email
                - slack
            target:
              type: string
              nullable: true
            next_run_at:
              type: string
              format: date-time
            last_run:
              type: string
              nullable: true
          nullable: true
      nullable: false
    BillingConnection:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - provider
        - livemode
        - status
        - external_account_id
        - business_name
        - capabilities
        - available
        - webhook_url
        - backfill
        - webhook_secret_set
        - settings
        - internal_id
        - last_event_at
        - error
        - created_at
      properties:
        object:
          type: string
          enum:
            - billing_connection
        id:
          type: string
          example: bcn_…
        provider:
          type: string
          enum:
            - stripe
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
        livemode:
          type: boolean
        status:
          type: string
          enum:
            - active
            - revoked
            - error
        external_account_id:
          type: string
          nullable: true
        business_name:
          type: string
          nullable: true
        capabilities:
          type: object
          required:
            - retries
            - card_update
            - offers
            - cancel_via_api
            - disputes
            - fraud_warnings
            - webhooks
            - backfill
          additionalProperties: false
          properties:
            retries:
              type: string
              enum:
                - nochurn
                - provider
                - none
            card_update:
              type: string
              enum:
                - hosted_page
                - provider_portal
                - none
            offers:
              type: array
              items:
                type: string
                enum:
                  - coupon
                  - pause
                  - downgrade
            cancel_via_api:
              type: boolean
            disputes:
              type: string
              enum:
                - evidence_api
                - provider_handles
                - refund_review
                - none
            fraud_warnings:
              type: boolean
            webhooks:
              type: boolean
            backfill:
              type: boolean
        available:
          type: boolean
          description: The provider's adapter is live on this NoChurn version.
        webhook_url:
          type: string
          nullable: true
          description: Paste it in the provider's webhook settings (not for Stripe).
        backfill:
          type: object
          additionalProperties: false
          required:
            - status
            - progress
            - started_at
            - finished_at
            - error
          properties:
            status:
              type: string
              enum:
                - pending
                - running
                - done
                - failed
            progress:
              type: object
              additionalProperties:
                type: integer
              description: Real counts imported so far, by object (customer, subscription, invoice, payment_method).
            started_at:
              type: string
              format: date-time
              nullable: true
            finished_at:
              type: string
              format: date-time
              nullable: true
            error:
              type: object
              additionalProperties: false
              required:
                - code
                - message
                - fix
                - docs_url
              properties:
                code:
                  type: string
                  enum:
                    - import_access_denied
                    - import_interrupted
                    - import_failed
                message:
                  type: string
                fix:
                  type: string
                docs_url:
                  type: string
              nullable: true
              description: Why the import stopped and what to do; null unless it failed.
          nullable: false
        webhook_secret_set:
          type: boolean
          description: "False until the provider's webhook secret is pasted (Recurly, Paddle, GoCardless, Razorpay): webhooks are refused until then. Always true for Stripe, for providers that sign webhooks with the API keys (Braintree) and for providers without a webhook secret (Mollie)."
        settings:
          type: object
          additionalProperties: false
          description: What the account declares for this connection (stores only; {} otherwise).
          properties:
            refund_review:
              type: object
              additionalProperties: false
              description: "Store refund reviews: NoChurn answers only if customer_consented is true (your app collects the consent); sample_content_provided is sent unless a free trial is seen on the purchase."
              properties:
                customer_consented:
                  type: boolean
                sample_content_provided:
                  type: boolean
            push_audience:
              type: string
              description: "Google Play: the audience of the Pub/Sub push OIDC token. Set, pushes without a valid Google-signed token are refused."
            push_service_account_email:
              type: string
              description: "Google Play: the service account that signs the push OIDC token."
        internal_id:
          type: object
          additionalProperties: false
          required:
            - field
            - defaults
            - fields
          properties:
            field:
              type: string
              nullable: true
              description: The field this connection names (PATCH internal_id_field); null = defaults.
              example: custom_fields.user_id
            defaults:
              type: array
              items:
                type: string
              example:
                - custom_fields.user_id
                - code
              description: Read in this order when field is null; user_id is the account's external_id_metadata_key.
            fields:
              type: array
              items:
                type: string
              example:
                - code
                - custom_fields.<name>
              description: The shapes a field may take for this tool (<name> = one key).
          nullable: true
          description: "Where NoChurn reads your app's id for a customer (your user id, stored as external_id) in this tool; null for a tool that carries none (stores, universal webhook). Nothing found: the email decides."
        last_event_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        webhook_setup:
          type: object
          nullable: true
          additionalProperties:
            type: string
          description: "Only in the response that creates the connection: webhook credentials to paste in the provider (e.g. Chargebee Basic auth). Shown once."
      nullable: false
    BillingProvider:
      type: object
      additionalProperties: false
      required:
        - object
        - provider
        - family
        - available
        - connect
        - credentials
        - capabilities
        - locked
        - webhook_secret
        - hint
        - docs_url
      properties:
        object:
          type: string
          enum:
            - billing_provider
        provider:
          type: string
          enum:
            - stripe
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
        family:
          type: string
          enum:
            - billing
            - merchant_of_record
            - payments
            - stores
            - custom
        available:
          type: boolean
          description: Its adapter ships in this version; otherwise "Coming soon".
        connect:
          type: string
          enum:
            - oauth
            - credentials
        credentials:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - name
              - secret
              - hint
            properties:
              name:
                type: string
              secret:
                type: boolean
              hint:
                type: string
              options:
                type: array
                items:
                  type: string
            nullable: false
        capabilities:
          type: object
          additionalProperties: true
          description: Default capabilities (see BillingConnection.capabilities).
        locked:
          type: array
          items:
            type: string
          description: Capabilities the provider decides (cannot be changed per connection).
        webhook_secret:
          type: string
          enum:
            - generated
            - pasted
            - none
          description: "generated: NoChurn shows webhook credentials once at creation; pasted: create the endpoint on webhook_url, then PATCH webhook_secret."
        hint:
          type: string
          nullable: true
          description: "What the provider needs beyond credentials, for the connection screen (stores: how customers get linked)."
        docs_url:
          type: string
          example: https://nochurn.sh/docs/integrations/lemon-squeezy/
          description: "The tool's integration guide: https://nochurn.sh/docs/integrations/<provider, _ written ->/."
      nullable: false
    BillingMapping:
      type: object
      additionalProperties: false
      required:
        - object
        - billing_connection
        - active
        - activated_at
        - rules
        - types
        - fields
        - transforms
        - sample
        - summary
      properties:
        object:
          type: string
          enum:
            - billing_mapping
        billing_connection:
          type: string
        active:
          type: boolean
        activated_at:
          type: string
          format: date-time
          nullable: true
        rules:
          type: array
          items:
            type: object
            additionalProperties: true
          description: '{ when: { path, equals | in }, type, event_id?, occurred_at?, fields: { "customer.id": "$.data.customer", "invoice.amount_due_cents": { path, transform } | { value } } }'
        types:
          type: array
          items:
            type: string
        fields:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        transforms:
          type: array
          items:
            type: string
        sample:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - id
              - received_at
              - payload
              - result
            properties:
              id:
                type: string
                example: bws_...
              received_at:
                type: string
                format: date-time
              payload:
                type: object
                additionalProperties: true
                description: The webhook as received (data, never code).
              result:
                type: object
                additionalProperties: false
                required:
                  - status
                  - rule
                  - type
                  - event_id
                  - data
                  - errors
                properties:
                  status:
                    type: string
                    enum:
                      - event
                      - ignored
                      - error
                    description: "event: recognized; ignored: no rule matched; error: a rule matched but a field failed."
                  rule:
                    type: integer
                    nullable: true
                    description: Index of the rule that matched.
                  type:
                    type: string
                    nullable: true
                  event_id:
                    type: string
                    nullable: true
                  data:
                    type: object
                    nullable: true
                    additionalProperties: true
                    description: The normalized event NoChurn would apply.
                  errors:
                    type: array
                    items:
                      type: string
                nullable: false
            nullable: false
        summary:
          type: object
          additionalProperties: false
          required:
            - total
            - events
            - errors
            - ignored
          properties:
            total:
              type: integer
            events:
              type: integer
            errors:
              type: integer
            ignored:
              type: integer
          nullable: false
      nullable: false
    Dispute:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - provider
        - billing_connection
        - external_id
        - charge
        - customer
        - customer_name
        - customer_email
        - customer_plan
        - amount_cents
        - currency
        - reason
        - status
        - evidence_due_by
        - opened_at
        - outcome_at
        - submitted_at
        - submitted_by
        - provider_submitted
        - read_only
        - capability
        - evidence_count
        - draft_status
        - win_score
        - created_at
      properties:
        object:
          type: string
          enum:
            - dispute
        id:
          type: string
          example: dsp_…
        livemode:
          type: boolean
        provider:
          type: string
          enum:
            - stripe
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
        billing_connection:
          type: string
        external_id:
          type: string
          example: dp_…
        charge:
          type: string
          nullable: true
          example: ch_…
        customer:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
        customer_email:
          type: string
          nullable: true
        customer_plan:
          type: string
          nullable: true
          description: Plan of the customer's latest subscription in this mode.
        amount_cents:
          type: integer
        currency:
          type: string
        reason:
          type: string
          nullable: true
          example: subscription_canceled
        status:
          type: string
          enum:
            - needs_response
            - under_review
            - won
            - lost
            - accepted
            - warning_needs_response
            - warning_under_review
            - warning_closed
            - prevented
            - no_decision
        evidence_due_by:
          type: string
          format: date-time
          nullable: true
        opened_at:
          type: string
          format: date-time
          nullable: true
        outcome_at:
          type: string
          format: date-time
          nullable: true
        submitted_at:
          type: string
          format: date-time
          nullable: true
        submitted_by:
          type: string
          nullable: true
          description: usr_… or system (auto-submit).
        provider_submitted:
          type: boolean
          description: "The provider already has a reply (Stripe Smart Disputes, its dashboard): NoChurn will not send."
        read_only:
          type: boolean
          description: "Nothing can be sent or edited: closed, submitted, or handled by the provider."
        capability:
          type: string
          enum:
            - evidence_api
            - provider_handles
            - refund_review
            - none
        evidence_count:
          type: integer
        draft_status:
          type: string
          nullable: true
          enum:
            - draft
            - approved
            - sent
            - null
        win_score:
          type: object
          nullable: true
          required:
            - score
            - source
            - confidence
            - factors
            - evidence_count
            - model
            - set_by
            - computed_at
          description: "Chance of winning, 0–100: the AI's estimate from the evidence (an estimate, not a measured probability), or a person's (source manual). Null until estimated."
          properties:
            score:
              type: integer
              minimum: 0
              maximum: 100
            source:
              type: string
              enum:
                - ai
                - manual
            confidence:
              type: string
              nullable: true
              enum:
                - low
                - medium
                - high
                - null
            factors:
              type: array
              items:
                type: object
                required:
                  - label
                  - effect
                properties:
                  label:
                    type: string
                  effect:
                    type: string
                    enum:
                      - up
                      - down
            evidence_count:
              type: integer
              nullable: true
            model:
              type: string
              nullable: true
            set_by:
              type: string
              nullable: true
            computed_at:
              type: string
              format: date-time
              nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    DisputeDetail:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - provider
        - billing_connection
        - external_id
        - charge
        - customer
        - customer_name
        - customer_email
        - customer_plan
        - amount_cents
        - currency
        - reason
        - status
        - evidence_due_by
        - opened_at
        - outcome_at
        - submitted_at
        - submitted_by
        - provider_submitted
        - read_only
        - capability
        - evidence_count
        - draft_status
        - win_score
        - created_at
        - evidence
        - draft
        - charge_details
        - recommendation
      properties:
        object:
          type: string
          enum:
            - dispute
        id:
          type: string
          example: dsp_…
        livemode:
          type: boolean
        provider:
          type: string
          enum:
            - stripe
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
        billing_connection:
          type: string
        external_id:
          type: string
          example: dp_…
        charge:
          type: string
          nullable: true
          example: ch_…
        customer:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
        customer_email:
          type: string
          nullable: true
        customer_plan:
          type: string
          nullable: true
          description: Plan of the customer's latest subscription in this mode.
        amount_cents:
          type: integer
        currency:
          type: string
        reason:
          type: string
          nullable: true
          example: subscription_canceled
        status:
          type: string
          enum:
            - needs_response
            - under_review
            - won
            - lost
            - accepted
            - warning_needs_response
            - warning_under_review
            - warning_closed
            - prevented
            - no_decision
        evidence_due_by:
          type: string
          format: date-time
          nullable: true
        opened_at:
          type: string
          format: date-time
          nullable: true
        outcome_at:
          type: string
          format: date-time
          nullable: true
        submitted_at:
          type: string
          format: date-time
          nullable: true
        submitted_by:
          type: string
          nullable: true
          description: usr_… or system (auto-submit).
        provider_submitted:
          type: boolean
          description: "The provider already has a reply (Stripe Smart Disputes, its dashboard): NoChurn will not send."
        read_only:
          type: boolean
          description: "Nothing can be sent or edited: closed, submitted, or handled by the provider."
        capability:
          type: string
          enum:
            - evidence_api
            - provider_handles
            - refund_review
            - none
        evidence_count:
          type: integer
        draft_status:
          type: string
          nullable: true
          enum:
            - draft
            - approved
            - sent
            - null
        win_score:
          type: object
          nullable: true
          required:
            - score
            - source
            - confidence
            - factors
            - evidence_count
            - model
            - set_by
            - computed_at
          description: "Chance of winning, 0–100: the AI's estimate from the evidence (an estimate, not a measured probability), or a person's (source manual). Null until estimated."
          properties:
            score:
              type: integer
              minimum: 0
              maximum: 100
            source:
              type: string
              enum:
                - ai
                - manual
            confidence:
              type: string
              nullable: true
              enum:
                - low
                - medium
                - high
                - null
            factors:
              type: array
              items:
                type: object
                required:
                  - label
                  - effect
                properties:
                  label:
                    type: string
                  effect:
                    type: string
                    enum:
                      - up
                      - down
            evidence_count:
              type: integer
              nullable: true
            model:
              type: string
              nullable: true
            set_by:
              type: string
              nullable: true
            computed_at:
              type: string
              format: date-time
              nullable: true
        created_at:
          type: string
          format: date-time
        evidence:
          type: array
          items:
            $ref: "#/components/schemas/DisputeEvidenceItem"
        draft:
          $ref: "#/components/schemas/DisputeDraft"
          nullable: true
        charge_details:
          type: object
          additionalProperties: false
          required:
            - paid_at
            - plan
            - invoice
            - period_end
          properties:
            paid_at:
              type: string
              format: date-time
              nullable: true
            plan:
              type: string
              nullable: true
            invoice:
              type: string
              nullable: true
              example: ninv_…
            period_end:
              type: string
              format: date-time
              nullable: true
              description: End of the invoiced period.
          nullable: true
          description: The disputed charge as NoChurn synced it; null when the payment is unknown.
        recommendation:
          type: object
          additionalProperties: false
          required:
            - suggestion
            - reason
            - evidence_count
            - amount_cents
            - contest_cost_cents
            - currency
            - win_score
            - expected_gain_cents
          properties:
            suggestion:
              type: string
              nullable: true
              enum:
                - accept
                - contest
                - null
              description: "accept: no evidence from a record and the amount is at most the cost of contesting. null: not enough to compare."
            reason:
              type: string
            evidence_count:
              type: integer
              description: Included evidence backed by a record (NoChurn's own statements excluded).
            amount_cents:
              type: integer
              nullable: true
              description: Disputed amount in `currency`; null without an exchange rate.
            contest_cost_cents:
              type: integer
              nullable: true
              description: From disputes_settings; null when not set.
            currency:
              type: string
            win_score:
              type: integer
              nullable: true
              description: Chance of winning used (AI or a person).
            expected_gain_cents:
              type: integer
              nullable: true
              description: Chance × amount, compared with the cost of contesting; null without a score and a cost.
          nullable: true
          description: "Whether contesting is worth it; null once nothing can be sent. A suggestion only: NoChurn never accepts by itself."
      nullable: false
    DisputeEvidenceItem:
      type: object
      additionalProperties: false
      required:
        - id
        - object
        - kind
        - title
        - content
        - source_type
        - source_id
        - occurred_at
        - included
        - position
        - file
      properties:
        id:
          type: string
          example: dev_…
        object:
          type: string
          enum:
            - dispute_evidence_item
        kind:
          type: string
          enum:
            - communication
            - policy
            - usage
            - reminder
            - cancel_history
            - refund_history
            - receipt
            - other
        title:
          type: string
        content:
          type: string
          nullable: true
        source_type:
          type: string
          nullable: true
          example: cancel_session
        source_id:
          type: string
          nullable: true
        occurred_at:
          type: string
          format: date-time
          nullable: true
        included:
          type: boolean
        position:
          type: integer
        file:
          type: object
          additionalProperties: false
          required:
            - filename
            - content_type
            - byte_size
            - sent_to_provider
          properties:
            filename:
              type: string
            content_type:
              type: string
              enum:
                - application/pdf
                - image/jpeg
                - image/png
            byte_size:
              type: integer
            sent_to_provider:
              type: boolean
              description: The provider has a copy (given when the reply was sent).
          nullable: true
          description: "A file attached by a person (source_type file): PDF, JPEG or PNG, 5 MB at most. null for other evidence."
      nullable: false
    DisputeDraft:
      type: object
      additionalProperties: false
      required:
        - id
        - object
        - text
        - cited_item_ids
        - model
        - status
        - edited_by
        - edited_at
        - created_at
      properties:
        id:
          type: string
          example: ddr_…
        object:
          type: string
          enum:
            - dispute_draft
        text:
          type: string
        cited_item_ids:
          type: array
          items:
            type: string
          description: Evidence items the reply relies on (dev_…).
        model:
          type: string
          nullable: true
          description: AI model, or "template" when the AI answer was rejected.
        status:
          type: string
          enum:
            - draft
            - approved
            - sent
        edited_by:
          type: string
          nullable: true
        edited_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: true
    DisputesSummary:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - days
        - open
        - due_within_72h
        - next_due_at
        - drafts_ready
        - open_amount
        - won
        - lost
        - decided
        - win_rate
        - win_score_accuracy
        - won_amount
        - fraud_warnings_open
        - fraud_warnings_refunded
      properties:
        object:
          type: string
          enum:
            - disputes_summary
        livemode:
          type: boolean
        days:
          type: integer
        open:
          type: integer
        due_within_72h:
          type: integer
        open_amount:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - currency
              - amount_cents
            properties:
              currency:
                type: string
              amount_cents:
                type: integer
            nullable: false
        next_due_at:
          type: string
          format: date-time
          nullable: true
          description: Soonest upcoming deadline of an open dispute.
        drafts_ready:
          type: integer
          description: "Open disputes NoChurn can send whose reply is drafted: they wait for a review."
        won:
          type: integer
        lost:
          type: integer
          description: Lost or accepted.
        decided:
          type: integer
          description: Sample size of win_rate.
        win_rate:
          type: number
          nullable: true
        won_amount:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - currency
              - amount_cents
            properties:
              currency:
                type: string
              amount_cents:
                type: integer
            nullable: false
          description: "Saved: disputes won and closed only."
        win_score_accuracy:
          type: object
          additionalProperties: false
          required:
            - decided
            - right
            - rate
          properties:
            decided:
              type: integer
              description: Sample size.
            right:
              type: integer
            rate:
              type: number
              nullable: true
          nullable: false
          description: How often the AI's chance of winning called the outcome (50+ = won), on decided disputes it scored.
        fraud_warnings_open:
          type: integer
        fraud_warnings_refunded:
          type: integer
      nullable: false
    FraudWarning:
      type: object
      additionalProperties: false
      required:
        - id
        - object
        - livemode
        - provider
        - external_id
        - charge
        - customer
        - customer_name
        - customer_email
        - customer_plan
        - amount_cents
        - currency
        - fraud_type
        - actionable
        - action
        - acted_by
        - acted_at
        - note
        - created_at
      properties:
        id:
          type: string
          example: efw_…
        object:
          type: string
          enum:
            - fraud_warning
        livemode:
          type: boolean
        provider:
          type: string
        external_id:
          type: string
          example: issfr_…
        charge:
          type: string
          nullable: true
        customer:
          type: string
          nullable: true
        customer_name:
          type: string
          nullable: true
        customer_email:
          type: string
          nullable: true
        customer_plan:
          type: string
          nullable: true
        amount_cents:
          type: integer
          nullable: true
        currency:
          type: string
          nullable: true
        fraud_type:
          type: string
          nullable: true
          example: made_with_stolen_card
        actionable:
          type: boolean
        action:
          type: string
          enum:
            - none
            - refunded
            - dismissed
        acted_by:
          type: string
          nullable: true
        acted_at:
          type: string
          format: date-time
          nullable: true
        note:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    FlowNotifications:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - events
        - slack
        - intercom
      properties:
        object:
          type: string
          enum:
            - flow_notifications
        livemode:
          type: boolean
        events:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - type
              - flow
            properties:
              type:
                type: string
                enum:
                  - recovery.opened
                  - recovery.recovered
                  - recovery.lost
                  - cancel_session.offer_accepted
                  - cancel_session.completed
                  - customer.flagged
                  - winback.reactivated
              flow:
                type: string
                enum:
                  - prevention
                  - recovery
                  - cancel
                  - winback
            nullable: false
          description: The events that can be notified, with the flow that emits them.
        slack:
          type: object
          additionalProperties: false
          required:
            - kind
            - connected
            - integration_status
            - workspace
            - events
            - channel
            - notes_access
            - last_delivery_at
            - last_error
          properties:
            kind:
              type: string
              enum:
                - slack
                - intercom_note
            connected:
              type: boolean
              description: The integration (Slack, Intercom) is connected and active.
            integration_status:
              type: string
              enum:
                - active
                - error
                - revoked
              nullable: true
              description: "null: never connected."
            workspace:
              type: string
              nullable: true
              description: The Slack workspace or Intercom app.
            events:
              type: array
              items:
                type: string
                enum:
                  - recovery.opened
                  - recovery.recovered
                  - recovery.lost
                  - cancel_session.offer_accepted
                  - cancel_session.completed
                  - customer.flagged
                  - winback.reactivated
              description: "Empty: nothing is sent."
            channel:
              type: string
              nullable: true
              description: "Slack only: where messages go (the channel chosen for Slack alerts until one is saved here)."
            notes_access:
              type: string
              enum:
                - unknown
                - ok
                - reconnect_required
                - null
              nullable: true
              description: "Intercom only: whether the install may write notes on contacts. reconnect_required: Intercom refused a note, reconnect it."
            last_delivery_at:
              type: string
              format: date-time
              nullable: true
            last_error:
              type: string
              nullable: true
              description: Why the last notification failed, with what to do.
          nullable: false
        intercom:
          type: object
          additionalProperties: false
          required:
            - kind
            - connected
            - integration_status
            - workspace
            - events
            - channel
            - notes_access
            - last_delivery_at
            - last_error
          properties:
            kind:
              type: string
              enum:
                - slack
                - intercom_note
            connected:
              type: boolean
              description: The integration (Slack, Intercom) is connected and active.
            integration_status:
              type: string
              enum:
                - active
                - error
                - revoked
              nullable: true
              description: "null: never connected."
            workspace:
              type: string
              nullable: true
              description: The Slack workspace or Intercom app.
            events:
              type: array
              items:
                type: string
                enum:
                  - recovery.opened
                  - recovery.recovered
                  - recovery.lost
                  - cancel_session.offer_accepted
                  - cancel_session.completed
                  - customer.flagged
                  - winback.reactivated
              description: "Empty: nothing is sent."
            channel:
              type: string
              nullable: true
              description: "Slack only: where messages go (the channel chosen for Slack alerts until one is saved here)."
            notes_access:
              type: string
              enum:
                - unknown
                - ok
                - reconnect_required
                - null
              nullable: true
              description: "Intercom only: whether the install may write notes on contacts. reconnect_required: Intercom refused a note, reconnect it."
            last_delivery_at:
              type: string
              format: date-time
              nullable: true
            last_error:
              type: string
              nullable: true
              description: Why the last notification failed, with what to do.
          nullable: false
      nullable: false
    OauthGrant:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - client
        - user
        - scopes
        - live_read
        - live_write
        - last_used_at
        - revoked_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - oauth_grant
        id:
          type: string
          example: ogr_V9xYpL4aZf3kQ8m2
        client:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - verified
            - verified_name
            - client_uri
          properties:
            id:
              type: string
            name:
              type: string
            verified:
              type: boolean
              description: true when every redirect URI of the client is a known callback of one vendor (Claude, ChatGPT, Cursor; Oauth::VerifiedClients), never because of its name. Loopback redirects are never verified.
            verified_name:
              type: string
              nullable: true
              enum:
                - Claude
                - ChatGPT
                - Cursor
                - null
              description: The vendor's canonical name when verified (show it instead of name), null otherwise.
            client_uri:
              type: string
              nullable: true
          nullable: false
        user:
          type: object
          additionalProperties: false
          required:
            - id
            - email
            - name
          properties:
            id:
              type: string
            email:
              type: string
            name:
              type: string
              nullable: true
          nullable: false
          description: Who connected the app. The app acts with this user's role.
        scopes:
          type: array
          items:
            type: string
            enum:
              - read
              - configure
              - sandbox
        live_read:
          type: boolean
          description: "The app may read live data (NoChurn-Mode: live, reads only). Off unless a human allowed it."
        live_write:
          type: string
          enum:
            - approval_required
          description: Changes in live mode always go through a live request.
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
      nullable: false
    LiveRequest:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - action
        - title
        - summary
        - params
        - precondition
        - reason
        - status
        - requested_by
        - approval_url
        - decided_by
        - decided_at
        - result
        - error
        - api_key
        - response
        - expires_at
        - created_at
      properties:
        object:
          type: string
          enum:
            - live_request
        id:
          type: string
          example: lrq_L4aZf3kQ8m2V9xYp
        action:
          type: string
          enum:
            - create_live_api_key
            - publish_flow_live
            - set_billing_source
            - create_webhook_endpoint
            - update_webhook_endpoint
            - delete_webhook_endpoint
            - test_webhook_endpoint
            - create_attribute_definition
            - update_attribute_definition
            - create_customer
            - update_customer
            - set_customer_attributes
            - record_usage_event
            - record_usage_events
            - create_subscription
            - update_subscription
            - record_payment
          description: The operation (LiveRequestCreate lists each one with its params).
        title:
          type: string
          description: The operation's name, e.g. Add a live webhook endpoint.
        summary:
          type: string
          description: What approving does, written by NoChurn from the params (never by the AI app).
        params:
          type: object
          additionalProperties: true
          description: The operation's params as checked by NoChurn (LiveRequestParams*), e.g. a customer reference resolved to its cst_… id.
        precondition:
          type: object
          additionalProperties: false
          required:
            - kind
            - object
            - id
            - expected
            - status
            - description
          properties:
            kind:
              type: string
              enum:
                - draft_digest
                - current_value
                - record_version
                - record_exists
            object:
              type: string
              nullable: true
              description: flow, billing_source, webhook_endpoint, attribute_definition, customer, subscription
            id:
              type: string
              nullable: true
            expected:
              type: string
              nullable: true
              description: "current_value only: the value when the app asked."
            status:
              type: string
              nullable: true
              enum:
                - holds
                - changed
                - null
              description: "Pending requests only: checked now."
            description:
              type: string
          nullable: true
          description: What must still hold at approval, else the request fails without acting.
        reason:
          type: string
          nullable: true
          description: "Written by the AI app: untrusted text, show it as text."
        status:
          type: string
          enum:
            - pending
            - approved
            - rejected
            - expired
            - failed
        requested_by:
          type: object
          additionalProperties: false
          required:
            - type
            - name
            - grant_id
          properties:
            type:
              type: string
              enum:
                - ai_agent
            name:
              type: string
            grant_id:
              type: string
              nullable: true
          nullable: false
        approval_url:
          type: string
          description: Dashboard page where an owner or admin approves or rejects it.
        decided_by:
          type: object
          additionalProperties: false
          required:
            - id
            - email
          properties:
            id:
              type: string
            email:
              type: string
          nullable: true
        decided_at:
          type: string
          format: date-time
          nullable: true
        result:
          type: object
          additionalProperties: true
          description: "What the approval produced: object and id only (plus api_key_id, flow_version_id, billing_source for the first three operations). Never live data."
        error:
          type: string
          nullable: true
          description: "failed: why the action did not run."
        api_key:
          allOf:
            - $ref: "#/components/schemas/ApiKey"
          nullable: true
          description: "Approval of create_live_api_key only: the new live key with its secret, shown once to the approver."
        response:
          type: object
          nullable: true
          additionalProperties: true
          description: "Approval answer only, to the approver: the operation's API response (it may hold a secret shown once: a live key's token, a webhook endpoint's signing secret). Never stored."
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
      nullable: false
    LiveRequestParamsCreateLiveApiKey:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        kind:
          type: string
          enum:
            - secret
            - publishable
        access:
          type: string
          enum:
            - full
            - restricted
        permissions:
          type: object
          additionalProperties:
            type: string
            enum:
              - none
              - read
              - write
      description: "Params of create_live_api_key: Create a live API key."
    LiveRequestCreateCreateLiveApiKey:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - create_live_api_key
        params:
          $ref: "#/components/schemas/LiveRequestParamsCreateLiveApiKey"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsPublishFlowLive:
      type: object
      additionalProperties: false
      required:
        - flow_id
      properties:
        flow_id:
          type: string
          minLength: 1
          maxLength: 255
          description: "flw_… or a kind: prevention, recovery, cancel, winback"
      description: "Params of publish_flow_live: Publish a flow to live mode."
    LiveRequestCreatePublishFlowLive:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - publish_flow_live
        params:
          $ref: "#/components/schemas/LiveRequestParamsPublishFlowLive"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsSetBillingSource:
      type: object
      additionalProperties: false
      required:
        - billing_source
      properties:
        billing_source:
          type: string
          enum:
            - stripe
            - api
      description: "Params of set_billing_source: Change the billing source."
    LiveRequestCreateSetBillingSource:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - set_billing_source
        params:
          $ref: "#/components/schemas/LiveRequestParamsSetBillingSource"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsCreateWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - url
      properties:
        url:
          type: string
          minLength: 8
          maxLength: 2000
        events:
          oneOf:
            - type: string
              enum:
                - "*"
            - type: array
              minItems: 1
              items:
                type: string
                enum:
                  - recovery.opened
                  - recovery.recovered
                  - recovery.lost
                  - outcome.confirmed
                  - outcome.reverted
                  - billing.retry_requested
                  - billing.cancel_requested
                  - billing.discount_requested
                  - billing.pause_requested
                  - billing.downgrade_requested
                  - customer.scored
                  - customer.flagged
                  - customer.access_restricted
                  - customer.access_restored
                  - cancel_session.started
                  - cancel_session.offer_accepted
                  - cancel_session.completed
                  - winback.reactivated
                  - "*"
        description:
          type: string
          maxLength: 255
      description: "Params of create_webhook_endpoint: Add a live webhook endpoint."
    LiveRequestCreateCreateWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - create_webhook_endpoint
        params:
          $ref: "#/components/schemas/LiveRequestParamsCreateWebhookEndpoint"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsUpdateWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        url:
          type: string
          minLength: 8
          maxLength: 2000
        events:
          oneOf:
            - type: string
              enum:
                - "*"
            - type: array
              minItems: 1
              items:
                type: string
                enum:
                  - recovery.opened
                  - recovery.recovered
                  - recovery.lost
                  - outcome.confirmed
                  - outcome.reverted
                  - billing.retry_requested
                  - billing.cancel_requested
                  - billing.discount_requested
                  - billing.pause_requested
                  - billing.downgrade_requested
                  - customer.scored
                  - customer.flagged
                  - customer.access_restricted
                  - customer.access_restored
                  - cancel_session.started
                  - cancel_session.offer_accepted
                  - cancel_session.completed
                  - winback.reactivated
                  - "*"
        description:
          type: string
          maxLength: 255
        status:
          type: string
          enum:
            - enabled
            - disabled
      description: "Params of update_webhook_endpoint: Change a live webhook endpoint."
    LiveRequestCreateUpdateWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - update_webhook_endpoint
        params:
          $ref: "#/components/schemas/LiveRequestParamsUpdateWebhookEndpoint"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsDeleteWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
      description: "Params of delete_webhook_endpoint: Remove a live webhook endpoint."
    LiveRequestCreateDeleteWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - delete_webhook_endpoint
        params:
          $ref: "#/components/schemas/LiveRequestParamsDeleteWebhookEndpoint"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsTestWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        type:
          type: string
          enum:
            - recovery.opened
            - recovery.recovered
            - recovery.lost
            - outcome.confirmed
            - outcome.reverted
            - billing.retry_requested
            - billing.cancel_requested
            - billing.discount_requested
            - billing.pause_requested
            - billing.downgrade_requested
            - customer.scored
            - customer.flagged
            - customer.access_restricted
            - customer.access_restored
            - cancel_session.started
            - cancel_session.offer_accepted
            - cancel_session.completed
            - winback.reactivated
      description: "Params of test_webhook_endpoint: Send a test webhook to a live endpoint."
    LiveRequestCreateTestWebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - test_webhook_endpoint
        params:
          $ref: "#/components/schemas/LiveRequestParamsTestWebhookEndpoint"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsCreateAttributeDefinition:
      type: object
      additionalProperties: false
      required:
        - key
        - type
      properties:
        key:
          type: string
          minLength: 1
          maxLength: 40
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - date
        sensitive:
          type: boolean
      description: "Params of create_attribute_definition: Declare a live customer attribute."
    LiveRequestCreateCreateAttributeDefinition:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - create_attribute_definition
        params:
          $ref: "#/components/schemas/LiveRequestParamsCreateAttributeDefinition"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsUpdateAttributeDefinition:
      type: object
      additionalProperties: false
      required:
        - key
        - sensitive
      properties:
        key:
          type: string
          minLength: 1
          maxLength: 40
        sensitive:
          type: boolean
      description: "Params of update_attribute_definition: Change a live customer attribute."
    LiveRequestCreateUpdateAttributeDefinition:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - update_attribute_definition
        params:
          $ref: "#/components/schemas/LiveRequestParamsUpdateAttributeDefinition"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsCreateCustomer:
      type: object
      additionalProperties: false
      required: []
      properties:
        external_id:
          type: string
          maxLength: 255
        email:
          type: string
          maxLength: 255
        name:
          type: string
          maxLength: 255
        locale:
          type: string
          maxLength: 255
        country:
          type: string
          maxLength: 255
        region:
          type: string
          maxLength: 255
        timezone:
          type: string
          maxLength: 255
      description: "Params of create_customer: Create a live customer."
    LiveRequestCreateCreateCustomer:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - create_customer
        params:
          $ref: "#/components/schemas/LiveRequestParamsCreateCustomer"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsUpdateCustomer:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        attributes:
          type: object
          description: Custom attributes; null removes a key.
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
            nullable: true
        external_id:
          type: string
          maxLength: 255
        email:
          type: string
          maxLength: 255
        name:
          type: string
          maxLength: 255
        locale:
          type: string
          maxLength: 255
        country:
          type: string
          maxLength: 255
        region:
          type: string
          maxLength: 255
        timezone:
          type: string
          maxLength: 255
      description: "Params of update_customer: Change a live customer."
    LiveRequestCreateUpdateCustomer:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - update_customer
        params:
          $ref: "#/components/schemas/LiveRequestParamsUpdateCustomer"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsSetCustomerAttributes:
      type: object
      additionalProperties: false
      required:
        - id
        - attributes
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        attributes:
          type: object
          description: Custom attributes; null removes a key.
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
            nullable: true
      description: "Params of set_customer_attributes: Set attributes on a live customer."
    LiveRequestCreateSetCustomerAttributes:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - set_customer_attributes
        params:
          $ref: "#/components/schemas/LiveRequestParamsSetCustomerAttributes"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsRecordUsageEvent:
      type: object
      additionalProperties: false
      required:
        - customer
        - name
      properties:
        customer:
          type: string
          minLength: 1
          maxLength: 255
        name:
          type: string
          minLength: 1
          maxLength: 100
        timestamp:
          type: string
          maxLength: 40
          description: ISO 8601
        properties:
          type: object
        idempotency_key:
          type: string
          maxLength: 255
      description: "Params of record_usage_event: Record a live usage event."
    LiveRequestCreateRecordUsageEvent:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - record_usage_event
        params:
          $ref: "#/components/schemas/LiveRequestParamsRecordUsageEvent"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsRecordUsageEvents:
      type: object
      additionalProperties: false
      required:
        - events
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            type: object
            additionalProperties: false
            required:
              - customer
              - name
            properties:
              customer:
                type: string
                minLength: 1
                maxLength: 255
              name:
                type: string
                minLength: 1
                maxLength: 100
              timestamp:
                type: string
                maxLength: 40
                description: ISO 8601
              properties:
                type: object
              idempotency_key:
                type: string
                maxLength: 255
      description: "Params of record_usage_events: Record live usage events."
    LiveRequestCreateRecordUsageEvents:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - record_usage_events
        params:
          $ref: "#/components/schemas/LiveRequestParamsRecordUsageEvents"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsCreateSubscription:
      type: object
      additionalProperties: false
      required:
        - external_id
        - customer
      properties:
        external_id:
          type: string
          maxLength: 255
        customer:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 255
            - type: object
              additionalProperties: false
              properties:
                external_id:
                  type: string
                  maxLength: 255
                email:
                  type: string
                  maxLength: 255
                name:
                  type: string
                  maxLength: 255
          description: cst_…, cus_… or your own id, or { external_id, email, name }
        plan:
          type: string
          maxLength: 255
        amount:
          type: integer
          minimum: 0
        currency:
          type: string
          minLength: 3
          maxLength: 3
        interval:
          type: string
          enum:
            - day
            - week
            - month
            - year
        interval_count:
          type: integer
          minimum: 1
        status:
          type: string
          maxLength: 255
        current_period_end:
          type: string
          maxLength: 40
          description: ISO 8601
        cancel_at_period_end:
          type: boolean
        started_at:
          type: string
          maxLength: 40
          description: ISO 8601
      description: "Params of create_subscription: Record a live subscription."
    LiveRequestCreateCreateSubscription:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - create_subscription
        params:
          $ref: "#/components/schemas/LiveRequestParamsCreateSubscription"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsUpdateSubscription:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        plan:
          type: string
          maxLength: 255
        amount:
          type: integer
          minimum: 0
        currency:
          type: string
          minLength: 3
          maxLength: 3
        interval:
          type: string
          enum:
            - day
            - week
            - month
            - year
        interval_count:
          type: integer
          minimum: 1
        status:
          type: string
          maxLength: 255
        current_period_end:
          type: string
          maxLength: 40
          description: ISO 8601
        cancel_at_period_end:
          type: boolean
        started_at:
          type: string
          maxLength: 40
          description: ISO 8601
      description: "Params of update_subscription: Change a live subscription."
    LiveRequestCreateUpdateSubscription:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - update_subscription
        params:
          $ref: "#/components/schemas/LiveRequestParamsUpdateSubscription"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestParamsRecordPayment:
      type: object
      additionalProperties: false
      required:
        - external_id
        - customer
        - amount
        - currency
        - status
      properties:
        external_id:
          type: string
          maxLength: 255
        customer:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 255
            - type: object
              additionalProperties: false
              properties:
                external_id:
                  type: string
                  maxLength: 255
                email:
                  type: string
                  maxLength: 255
                name:
                  type: string
                  maxLength: 255
          description: cst_…, cus_… or your own id, or { external_id, email, name }
        subscription:
          type: string
          minLength: 1
          maxLength: 255
        invoice:
          type: string
          minLength: 1
          maxLength: 255
        amount:
          type: integer
          minimum: 0
        currency:
          type: string
          minLength: 3
          maxLength: 3
        status:
          type: string
          enum:
            - failed
            - succeeded
        decline_code:
          type: string
          maxLength: 255
        failure_code:
          type: string
          maxLength: 255
        occurred_at:
          type: string
          maxLength: 40
          description: ISO 8601
        invoice_url:
          type: string
          maxLength: 2000
      description: "Params of record_payment: Record a live payment attempt."
    LiveRequestCreateRecordPayment:
      type: object
      additionalProperties: false
      required:
        - action
        - params
      properties:
        action:
          type: string
          enum:
            - record_payment
        params:
          $ref: "#/components/schemas/LiveRequestParamsRecordPayment"
        reason:
          type: string
          maxLength: 1000
          description: Shown to the human as the app's note (untrusted text).
      nullable: false
    LiveRequestCreate:
      oneOf:
        - $ref: "#/components/schemas/LiveRequestCreateCreateLiveApiKey"
        - $ref: "#/components/schemas/LiveRequestCreatePublishFlowLive"
        - $ref: "#/components/schemas/LiveRequestCreateSetBillingSource"
        - $ref: "#/components/schemas/LiveRequestCreateCreateWebhookEndpoint"
        - $ref: "#/components/schemas/LiveRequestCreateUpdateWebhookEndpoint"
        - $ref: "#/components/schemas/LiveRequestCreateDeleteWebhookEndpoint"
        - $ref: "#/components/schemas/LiveRequestCreateTestWebhookEndpoint"
        - $ref: "#/components/schemas/LiveRequestCreateCreateAttributeDefinition"
        - $ref: "#/components/schemas/LiveRequestCreateUpdateAttributeDefinition"
        - $ref: "#/components/schemas/LiveRequestCreateCreateCustomer"
        - $ref: "#/components/schemas/LiveRequestCreateUpdateCustomer"
        - $ref: "#/components/schemas/LiveRequestCreateSetCustomerAttributes"
        - $ref: "#/components/schemas/LiveRequestCreateRecordUsageEvent"
        - $ref: "#/components/schemas/LiveRequestCreateRecordUsageEvents"
        - $ref: "#/components/schemas/LiveRequestCreateCreateSubscription"
        - $ref: "#/components/schemas/LiveRequestCreateUpdateSubscription"
        - $ref: "#/components/schemas/LiveRequestCreateRecordPayment"
      discriminator:
        propertyName: action
        mapping:
          create_live_api_key: "#/components/schemas/LiveRequestCreateCreateLiveApiKey"
          publish_flow_live: "#/components/schemas/LiveRequestCreatePublishFlowLive"
          set_billing_source: "#/components/schemas/LiveRequestCreateSetBillingSource"
          create_webhook_endpoint: "#/components/schemas/LiveRequestCreateCreateWebhookEndpoint"
          update_webhook_endpoint: "#/components/schemas/LiveRequestCreateUpdateWebhookEndpoint"
          delete_webhook_endpoint: "#/components/schemas/LiveRequestCreateDeleteWebhookEndpoint"
          test_webhook_endpoint: "#/components/schemas/LiveRequestCreateTestWebhookEndpoint"
          create_attribute_definition: "#/components/schemas/LiveRequestCreateCreateAttributeDefinition"
          update_attribute_definition: "#/components/schemas/LiveRequestCreateUpdateAttributeDefinition"
          create_customer: "#/components/schemas/LiveRequestCreateCreateCustomer"
          update_customer: "#/components/schemas/LiveRequestCreateUpdateCustomer"
          set_customer_attributes: "#/components/schemas/LiveRequestCreateSetCustomerAttributes"
          record_usage_event: "#/components/schemas/LiveRequestCreateRecordUsageEvent"
          record_usage_events: "#/components/schemas/LiveRequestCreateRecordUsageEvents"
          create_subscription: "#/components/schemas/LiveRequestCreateCreateSubscription"
          update_subscription: "#/components/schemas/LiveRequestCreateUpdateSubscription"
          record_payment: "#/components/schemas/LiveRequestCreateRecordPayment"
    SandboxCustomer:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - subscription
        - payment_method
        - next
      properties:
        object:
          type: string
          enum:
            - sandbox_customer
        customer:
          $ref: "#/components/schemas/Customer"
        subscription:
          $ref: "#/components/schemas/Subscription"
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        next:
          type: string
      nullable: false
    SandboxFailedPayment:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - invoice
        - payment
        - recovery_run
        - skipped
        - next
      properties:
        object:
          type: string
          enum:
            - sandbox_failed_payment
        customer:
          type: string
        invoice:
          $ref: "#/components/schemas/Invoice"
        payment:
          $ref: "#/components/schemas/Payment"
        recovery_run:
          type: object
          additionalProperties: false
          required:
            - id
            - state
            - holdout
            - stop_reason
          properties:
            id:
              type: string
            state:
              type: string
            holdout:
              type: boolean
            stop_reason:
              type: string
              nullable: true
          nullable: true
        skipped:
          type: object
          additionalProperties: false
          required:
            - code
            - reason
          properties:
            code:
              type: string
              nullable: true
            reason:
              type: string
              nullable: true
          nullable: true
          description: Why no recovery run started (e.g. not_published).
        next:
          type: string
      nullable: false
    SandboxCardRemoved:
      type: object
      additionalProperties: false
      required:
        - object
        - customer
        - payment_method
        - flagged
        - prevention_run
      properties:
        object:
          type: string
          enum:
            - sandbox_card_removed
        customer:
          $ref: "#/components/schemas/Customer"
        payment_method:
          $ref: "#/components/schemas/PaymentMethod"
        flagged:
          type: boolean
        prevention_run:
          type: object
          additionalProperties: false
          required:
            - id
            - state
            - holdout
            - stop_reason
          properties:
            id:
              type: string
            state:
              type: string
            holdout:
              type: boolean
            stop_reason:
              type: string
              nullable: true
          nullable: true
      nullable: false
    SandboxCancelFlowRun:
      type: object
      additionalProperties: false
      required:
        - object
        - session
      properties:
        object:
          type: string
          enum:
            - sandbox_cancel_flow_run
        session:
          $ref: "#/components/schemas/CancelSession"
      nullable: false
    IntegrationVerification:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - complete
        - passed
        - total
        - checks
        - go_live_url
      properties:
        object:
          type: string
          enum:
            - integration_verification
        livemode:
          type: boolean
          enum:
            - false
        complete:
          type: boolean
        passed:
          type: integer
        total:
          type: integer
        checks:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - status
              - detail
              - fix
            properties:
              key:
                type: string
                enum:
                  - billing_connected
                  - webhook_verified
                  - identify_received
                  - cancel_flow_opened
                  - click_to_cancel
                  - failed_payment_email
                  - no_secret_in_code
              status:
                type: string
                enum:
                  - pass
                  - fail
                  - unknown
                description: "unknown: NoChurn cannot tell yet (email being written, no code sent)."
              detail:
                type: string
              fix:
                type: string
                nullable: true
                description: What to do next; null when it passes.
            nullable: false
        go_live_url:
          type: string
          nullable: true
          description: Where a human goes live, once everything passes.
      nullable: false
    ProjectDetection:
      type: object
      additionalProperties: false
      required:
        - object
        - platforms
        - backend
        - frontend
        - mobile
        - billing
        - plan
      properties:
        object:
          type: string
          enum:
            - project_detection
        platforms:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - key
              - name
              - kind
              - evidence
            properties:
              key:
                type: string
                example: nextjs
              name:
                type: string
              kind:
                type: string
                enum:
                  - web
                  - web_front
                  - backend
                  - ios
                  - android
                  - cross_platform
              evidence:
                type: array
                items:
                  type: string
            nullable: false
        backend:
          type: string
          nullable: true
        frontend:
          type: string
          nullable: true
        mobile:
          type: array
          items:
            type: string
        billing:
          type: object
          additionalProperties: false
          required:
            - stripe
            - store_subscriptions
          properties:
            stripe:
              type: boolean
            store_subscriptions:
              type: object
              additionalProperties: false
              required:
                - detected
                - stores
                - evidence
                - note
              properties:
                detected:
                  type: boolean
                stores:
                  type: array
                  items:
                    type: string
                    enum:
                      - app_store
                      - google_play
                evidence:
                  type: array
                  items:
                    type: string
                note:
                  type: string
                  nullable: true
              nullable: false
          nullable: false
        plan:
          type: object
          additionalProperties: false
          required:
            - steps
            - warnings
          properties:
            steps:
              type: array
              items:
                type: object
                required:
                  - key
                  - title
                  - status
                  - tools
                  - snippet_cases
                properties:
                  key:
                    type: string
                  title:
                    type: string
                  status:
                    type: string
                    enum:
                      - available
                      - not_available_yet
                  tools:
                    type: array
                    items:
                      type: string
                  snippet_cases:
                    type: array
                    items:
                      type: string
                  snippet_platform:
                    type: string
                  detail:
                    type: string
            warnings:
              type: array
              items:
                type: string
          nullable: false
      nullable: false
    IntegrationSnippets:
      type: object
      additionalProperties: false
      required:
        - object
        - version
        - platform
        - language
        - api_base_url
        - snippets
      properties:
        object:
          type: string
          enum:
            - integration_snippets
        version:
          type: string
          example: 2026-11-06.2
        platform:
          type: string
          nullable: true
        language:
          type: string
          nullable: true
        api_base_url:
          type: string
        snippets:
          type: array
          items:
            type: object
            required:
              - case
              - status
              - title
              - language
              - code
            properties:
              case:
                type: string
              status:
                type: string
                enum:
                  - available
                  - not_available_yet
              title:
                type: string
              language:
                type: string
                nullable: true
                enum:
                  - node
                  - ruby
                  - python
                  - curl
                  - browser
                  - null
              label:
                type: string
              syntax:
                type: string
              runs_on:
                type: string
                enum:
                  - server
                  - browser
              insert:
                type: string
                description: Where to put it in the project.
              env:
                type: array
                items:
                  type: string
                description: Environment variables the code reads.
              file:
                type: string
              code:
                type: string
                nullable: true
              detail:
                type: string
      nullable: false
    ClientVersions:
      type: object
      additionalProperties: false
      required:
        - object
        - livemode
        - window_days
        - data
      properties:
        object:
          type: string
          enum:
            - client_versions
        livemode:
          type: boolean
        window_days:
          type: integer
          example: 90
        data:
          type: array
          items:
            type: object
            additionalProperties: false
            required:
              - object
              - client
              - name
              - latest
              - minimum
              - status
              - versions
            properties:
              object:
                type: string
                enum:
                  - client
              client:
                type: string
                example: widget
              name:
                type: string
                nullable: true
                example: Cancel widget (js.nochurn.sh)
              latest:
                type: string
                nullable: true
                example: 1.0.0
                description: null for a client NoChurn does not publish.
              minimum:
                type: string
                nullable: true
                example: 1.0.0
                description: Older versions are unsupported.
              status:
                type: string
                enum:
                  - current
                  - update_available
                  - unsupported
                  - unknown
                description: The worst status among the versions seen in the last 7 days, else the last version's.
              versions:
                type: array
                items:
                  type: object
                  additionalProperties: false
                  required:
                    - id
                    - version
                    - status
                    - origin_host
                    - api_key
                    - oauth_client
                    - first_seen_at
                    - last_seen_at
                    - calls_count
                  properties:
                    id:
                      type: string
                      example: clv_4fJ8sK2pQ9wLm3Xz
                    version:
                      type: string
                      example: 1.0.0
                    status:
                      type: string
                      enum:
                        - current
                        - update_available
                        - unsupported
                        - unknown
                      description: current; update_available (below latest); unsupported (below minimum); unknown (a client NoChurn does not publish).
                    origin_host:
                      type: string
                      nullable: true
                      example: app.acme.com
                      description: "Widget only: the host of the page's Origin (never a path)."
                    api_key:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - kind
                        - mode
                        - last4
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                          nullable: true
                        kind:
                          type: string
                          enum:
                            - secret
                            - publishable
                        mode:
                          type: string
                          enum:
                            - test
                            - live
                        last4:
                          type: string
                      nullable: true
                    oauth_client:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                      nullable: true
                      description: The AI app, for the MCP server through OAuth.
                    first_seen_at:
                      type: string
                      format: date-time
                    last_seen_at:
                      type: string
                      format: date-time
                    calls_count:
                      type: integer
                      description: "Approximate (a lower bound): written at most once every 5 minutes per row."
                  nullable: false
                description: Newest first.
            nullable: false
      nullable: false
    LogEntryDetail:
      type: object
      additionalProperties: false
      required:
        - object
        - id
        - livemode
        - occurred_at
        - source
        - type
        - customer
        - summary
        - status
        - error
        - why
        - ref
        - request_id
        - payload
      properties:
        object:
          type: string
          enum:
            - log_entry
        id:
          type: string
          example: log_3kH9sQ2pVb7LmXz1
        livemode:
          type: boolean
        occurred_at:
          type: string
          format: date-time
          description: Millisecond precision.
        source:
          type: string
          enum:
            - stripe
            - api
            - nochurn
            - widget
            - webhook
            - chargebee
            - recurly
            - maxio
            - zuora
            - paddle
            - lemon_squeezy
            - polar
            - braintree
            - mollie
            - gocardless
            - razorpay
            - app_store
            - google_play
            - custom
            - intercom
            - crisp
            - zendesk
            - hubspot
            - helpscout
            - gorgias
          description: "stripe (webhooks), api (your calls with an API key), nochurn (decisions), widget, webhook (outgoing), a billing tool (chargebee, zuora…: its webhooks and what NoChurn reads from it), a support tool (intercom, crisp, zendesk, hubspot, helpscout, gorgias)."
        type:
          type: string
          example: invoice.payment_failed
        customer:
          type: object
          additionalProperties: false
          required:
            - id
            - name
            - email
          properties:
            id:
              type: string
              example: cst_4fJ8sK2pQ9wLm3Xz
            name:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
          nullable: true
          description: The customer this line is about (open it with /v1/customers/{id}).
        summary:
          type: string
          nullable: true
          example: $49.00 · insufficient_funds
        status:
          type: string
          example: received
          description: HTTP status ("200", "500") or ok, received, ignored, failed.
        error:
          type: boolean
        why:
          type: string
          nullable: true
          description: Why this happened (the reason of the NoChurn decision).
        ref:
          type: object
          additionalProperties: false
          required:
            - type
            - id
          properties:
            type:
              type: string
              example: event
            id:
              type: string
              example: nevt_tZCYqtEDVAvwZjD5
          nullable: true
        request_id:
          type: string
          nullable: true
        payload:
          type: object
          additionalProperties: true
          description: Secrets, tokens and emails are filtered.
      nullable: false
    ApiKeyList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/ApiKey"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    EventList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Event"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    StripeConnectionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/StripeConnection"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    CustomerDuplicateList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/CustomerDuplicate"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SubscriptionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Subscription"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    InvoiceList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Invoice"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    PaymentMethodList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/PaymentMethod"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    MembershipList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Membership"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    InvitationList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Invitation"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    FlowList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Flow"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    FlowVersionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/FlowVersion"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    FlowRunList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/FlowRun"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SendingDomainList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/SendingDomain"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    MessageList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Message"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    EmailSuppressionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/EmailSuppression"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    WebhookEndpointList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEndpoint"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    WebhookDeliveryList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    PaymentList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Payment"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    OfferList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Offer"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    ReviewPlatformList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/ReviewPlatform"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SendScheduleFlowOverrideList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/SendScheduleFlowOverride"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    AttributeDefinitionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/AttributeDefinition"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    ImportList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Import"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SegmentList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Segment"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SegmentFieldList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/SegmentField"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    ExperimentList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Experiment"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    CancelSessionReportList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/CancelSessionReport"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    IntegrationList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Integration"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    QuerySourceList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/QuerySource"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    SavedQueryList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/SavedQuery"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    BillingConnectionList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/BillingConnection"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    BillingProviderList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/BillingProvider"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    DisputeList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/Dispute"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    FraudWarningList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/FraudWarning"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    OauthGrantList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/OauthGrant"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
    LiveRequestList:
      type: object
      additionalProperties: false
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          enum:
            - list
        data:
          type: array
          items:
            $ref: "#/components/schemas/LiveRequest"
        has_more:
          type: boolean
        next_cursor:
          type: string
          nullable: true
          description: Pass as starting_after to get the next page.
      nullable: false
paths:
  /v1/account:
    get:
      summary: The account of the API key (or of the dashboard session)
      tags:
        - Account
      operationId: getAccount
      responses:
        "200":
          description: account
          content:
            application/json:
              example:
                object: account
                id: acc_pApr4brLkk4KGSjf
                name: Account 1
                billing_source: stripe
                sandbox: true
                default_locale: en
                plan: free
                external_id_metadata_key: user_id
                require_two_factor: false
                timezone: Europe/Paris
                audience: null
                reporting_currency: null
                onboarding:
                  step: billing
                  skipped: []
                  completed_at: null
                disputes_settings:
                  auto_submit: false
                  auto_submit_hours_before: 24
                  auto_refund_fraud_warnings: false
                  auto_refund_max_cents: null
                  cancel_subscription_on_fraud_refund: true
                  notify_email: true
                  notify_slack: false
                  contest_cost_cents: null
                  contest_cost_currency: null
                  auto_submit_min_score: 70
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Account"
        "401":
          description: missing key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    patch:
      summary: Update account settings
      tags:
        - Account
      operationId: updateAccount
      parameters: []
      responses:
        "200":
          description: amounts reported in another currency
          content:
            application/json:
              example:
                object: account
                id: acc_hM7usMgmWPctiKMY
                name: Renamed
                billing_source: stripe
                sandbox: true
                default_locale: en
                plan: free
                external_id_metadata_key: app_user_id
                require_two_factor: false
                timezone: Europe/Paris
                audience: null
                reporting_currency: null
                onboarding:
                  step: billing
                  skipped: []
                  completed_at: null
                disputes_settings:
                  auto_submit: false
                  auto_submit_hours_before: 24
                  auto_refund_fraud_warnings: false
                  auto_refund_max_cents: null
                  cancel_subscription_on_fraud_refund: true
                  notify_email: true
                  notify_slack: false
                  contest_cost_cents: null
                  contest_cost_currency: null
                  auto_submit_min_score: 70
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Account"
        "400":
          description: a step that cannot be skipped
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: unknown time zone
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                default_locale:
                  type: string
                  enum:
                    - en
                    - de
                    - fr
                    - it
                    - pt
                billing_source:
                  type: string
                  enum:
                    - stripe
                    - api
                external_id_metadata_key:
                  type: string
                require_two_factor:
                  type: boolean
                  description: Owners only, from a dashboard session with 2FA on.
                timezone:
                  type: string
                  example: Europe/Paris
                  description: IANA time zone.
                reporting_currency:
                  type: string
                  nullable: true
                  example: usd
                  description: "Currency every amount is converted into (one with ECB rates). null: Stripe's default currency."
                onboarding:
                  type: object
                  description: Where the onboarding resumes. completed_at is set when step becomes done.
                  properties:
                    step:
                      type: string
                      enum:
                        - billing
                        - support
                        - cancel_modal
                        - email_domain
                        - done
                    skipped:
                      type: array
                      items:
                        type: string
                        enum:
                          - support
                          - cancel_modal
                          - email_domain
                disputes_settings:
                  type: object
                  description: Chargebacks › Settings (owner / admin). Only the keys sent change.
                  properties:
                    auto_submit:
                      type: boolean
                    auto_submit_hours_before:
                      type: integer
                      minimum: 1
                      maximum: 168
                    auto_refund_fraud_warnings:
                      type: boolean
                    auto_refund_max_cents:
                      type: integer
                      nullable: true
                    cancel_subscription_on_fraud_refund:
                      type: boolean
                    notify_email:
                      type: boolean
                    notify_slack:
                      type: boolean
                    contest_cost_cents:
                      type: integer
                      nullable: true
                    contest_cost_currency:
                      type: string
                      nullable: true
                    auto_submit_min_score:
                      type: integer
                      minimum: 0
                      maximum: 100
            example:
              name: Renamed
              external_id_metadata_key: app_user_id
        required: true
  /v1/sandbox/customers:
    post:
      summary: "Sandbox: create a test customer"
      tags:
        - Sandbox
      operationId: createSandboxCustomer
      description: Test mode only. A customer with an active subscription billed by API (no Stripe call) renewing in renews_in_days (5), and a default test card (visa 4242). Writes sandbox.customer_created.
      parameters: []
      responses:
        "201":
          description: created
          content:
            application/json:
              example:
                object: sandbox_customer
                customer:
                  object: customer
                  id: cst_bunoEakFaTiPtdny
                  stripe_customer_id: null
                  external_id: sandbox_204d2c04
                  stripe_account: null
                  livemode: false
                  email: sandbox+204d2c04@example.com
                  company_domain: null
                  name: Sandbox customer
                  locale: null
                  country: null
                  region: null
                  timezone: Europe/Paris
                  timezone_source: account
                  attributes:
                    signup_source: ads
                  churn_score: null
                  churn_level: null
                  flag: null
                  state: active
                  plan: Team
                  mrr_cents: 4900
                  lifetime_value_cents: 0
                  currency: usd
                  payment_method:
                    id: npm_wv5NxAxFfWoidwEv
                    type: card
                    brand: visa
                    last4: "4242"
                    exp_month: 12
                    exp_year: 2029
                    funding: credit
                    wallet: null
                    risk_category: null
                    label: Visa •••• 4242
                  last_seen_at: null
                  churned_at: null
                  flows_paused_at: null
                  signup_at: 2026-11-04T10:00:00.000Z
                  created_at: 2026-11-04T10:00:00.000Z
                  updated_at: 2026-11-04T10:00:00.000Z
                  identifiers:
                    - kind: email
                      provider: null
                      connection: null
                      value: sandbox+204d2c04@example.com
                      source: api
                      created_at: 2026-11-04T10:00:00.000Z
                    - kind: external
                      provider: null
                      connection: null
                      value: sandbox_204d2c04
                      source: api
                      created_at: 2026-11-04T10:00:00.000Z
                subscription:
                  object: subscription
                  id: nsub_ee6FgdZrth7eEGKc
                  customer: cst_bunoEakFaTiPtdny
                  stripe_subscription_id: null
                  external_id: sandbox_sub_204d2c04
                  livemode: false
                  status: active
                  plan: Team
                  amount_cents: 4900
                  currency: usd
                  interval: month
                  interval_count: 1
                  mrr_cents: 4900
                  current_period_end: 2026-11-09T10:00:00.000Z
                  cancel_at_period_end: false
                  canceled_at: null
                  paused_until: null
                  trial_end: null
                  created_at: 2026-11-04T10:00:00.000Z
                payment_method:
                  object: payment_method
                  id: npm_wv5NxAxFfWoidwEv
                  customer: cst_bunoEakFaTiPtdny
                  stripe_payment_method_id: null
                  livemode: false
                  type: card
                  brand: visa
                  last4: "4242"
                  exp_month: 12
                  exp_year: 2029
                  funding: credit
                  country: null
                  wallet: null
                  is_default: true
                  detached_at: null
                  created_at: 2026-11-04T10:00:00.000Z
                next: "Play a scenario: POST /v1/sandbox/failed_payment, /card_removed or /cancel_flow_run with customer: sandbox_204d2c04."
              schema:
                $ref: "#/components/schemas/SandboxCustomer"
        "403":
          description: live mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                external_id:
                  type: string
                email:
                  type: string
                name:
                  type: string
                locale:
                  type: string
                plan:
                  type: string
                  example: Pro
                amount:
                  type: integer
                  example: 4900
                  description: Cents.
                currency:
                  type: string
                  example: usd
                interval:
                  type: string
                  enum:
                    - month
                    - year
                renews_in_days:
                  type: integer
                  minimum: 1
                  maximum: 365
                attributes:
                  type: object
                  additionalProperties: true
                  example:
                    signup_source: ads
            example:
              plan: Team
              attributes:
                signup_source: ads
  /v1/sandbox/failed_payment:
    post:
      summary: "Sandbox: simulate a failed payment"
      tags:
        - Sandbox
      operationId: simulateFailedPayment
      description: "Test mode only. An open invoice and a failed payment, then the recovery flow like a failure sent by API: the day-0 email is written in the background (never sent to a real inbox). invoice_url is the card update page of your app (billing by API); without it the email waits for review without a card link."
      parameters: []
      responses:
        "201":
          description: recovery run started
          content:
            application/json:
              example:
                object: sandbox_failed_payment
                customer: cst_bKA5uydA3TFbny8p
                invoice:
                  object: invoice
                  id: ninv_r3bxXGhL1pmwzzsq
                  customer: cst_bKA5uydA3TFbny8p
                  subscription: nsub_h4fGLF2jw3Fmj949
                  stripe_invoice_id: null
                  external_id: sandbox_in_5c71a02c
                  livemode: false
                  number: null
                  status: open
                  amount_due_cents: 4900
                  amount_paid_cents: 0
                  amount_remaining_cents: 4900
                  currency: usd
                  attempt_count: 1
                  next_attempt_at: null
                  failure_code: null
                  decline_code: insufficient_funds
                  hosted_invoice_url: https://app.example.com/billing
                  paid_at: null
                  created_at: 2026-11-04T10:00:00.000Z
                payment:
                  object: payment
                  id: npay_36FrWRqwQscjZzbo
                  livemode: false
                  customer: cst_bKA5uydA3TFbny8p
                  subscription: nsub_h4fGLF2jw3Fmj949
                  invoice: ninv_r3bxXGhL1pmwzzsq
                  external_id: sandbox_pay_5c71a02c
                  stripe_charge_id: null
                  status: failed
                  amount_cents: 4900
                  currency: usd
                  decline_code: insufficient_funds
                  failure_code: null
                  occurred_at: 2026-11-04T10:00:00Z
                  created_at: 2026-11-04T10:00:00Z
                recovery_run:
                  id: run_mmJr34RXHjDV6MZi
                  state: active
                  holdout: false
                  stop_reason: null
                skipped: null
                next: "The day-0 email is written in the background (test mode: never sent to a real inbox). Check it with GET /v1/integration/verify."
              schema:
                $ref: "#/components/schemas/SandboxFailedPayment"
        "404":
          description: unknown customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: string
                  description: cst_… or your own id.
                amount:
                  type: integer
                currency:
                  type: string
                decline_code:
                  type: string
                  example: insufficient_funds
                invoice_url:
                  type: string
                  format: uri
            example:
              customer: cst_bKA5uydA3TFbny8p
              invoice_url: https://app.example.com/billing
        required: true
  /v1/sandbox/card_removed:
    post:
      summary: "Sandbox: the customer removes their card before the renewal"
      tags:
        - Sandbox
      operationId: simulateCardRemoved
      description: "Test mode only, on a sandbox card. Prevention signal card_removed: the customer is flagged, and the prevention flow writes its reminder when it is published in test mode."
      parameters: []
      responses:
        "201":
          description: flagged
          content:
            application/json:
              example:
                object: sandbox_card_removed
                customer:
                  object: customer
                  id: cst_m7f9kZH5XcRMAx7x
                  stripe_customer_id: null
                  external_id: sandbox_a096134f
                  stripe_account: null
                  livemode: false
                  email: sandbox+a096134f@example.com
                  company_domain: null
                  name: Sandbox customer
                  locale: null
                  country: null
                  region: null
                  timezone: Europe/Paris
                  timezone_source: account
                  attributes: {}
                  churn_score: 60
                  churn_level: medium
                  flag:
                    reason: card_removed
                    flagged_at: 2026-11-04T10:00:00.000Z
                  state: active
                  plan: Pro
                  mrr_cents: 4900
                  lifetime_value_cents: 0
                  currency: usd
                  payment_method: null
                  last_seen_at: null
                  churned_at: null
                  flows_paused_at: null
                  signup_at: 2026-11-04T10:00:00.000Z
                  created_at: 2026-11-04T10:00:00.000Z
                  updated_at: 2026-11-04T10:00:00.000Z
                  identifiers:
                    - kind: email
                      provider: null
                      connection: null
                      value: sandbox+a096134f@example.com
                      source: api
                      created_at: 2026-11-04T10:00:00.000Z
                    - kind: external
                      provider: null
                      connection: null
                      value: sandbox_a096134f
                      source: api
                      created_at: 2026-11-04T10:00:00.000Z
                payment_method:
                  object: payment_method
                  id: npm_r3X9MXd9dt36G7ZR
                  customer: cst_m7f9kZH5XcRMAx7x
                  stripe_payment_method_id: null
                  livemode: false
                  type: card
                  brand: visa
                  last4: "4242"
                  exp_month: 12
                  exp_year: 2029
                  funding: credit
                  country: null
                  wallet: null
                  is_default: false
                  detached_at: 2026-11-04T10:00:00.000Z
                  created_at: 2026-11-04T10:00:00.000Z
                flagged: true
                prevention_run: null
              schema:
                $ref: "#/components/schemas/SandboxCardRemoved"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: string
            example:
              customer: cst_m7f9kZH5XcRMAx7x
        required: true
  /v1/sandbox/cancel_flow_run:
    post:
      summary: "Sandbox: play the cancel flow"
      tags:
        - Sandbox
      operationId: runSandboxCancelFlow
      description: "Test mode only. Opens the cancel flow, answers a reason (the first one by default), refuses or accepts the offer, then cancels in one click (click to cancel). Billing by API: the cancellation reaches your app as billing.cancel_requested."
      parameters: []
      responses:
        "201":
          description: cancelled
          content:
            application/json:
              example:
                object: sandbox_cancel_flow_run
                session:
                  object: cancel_session
                  id: ses_E4ydMSMHe2opxRar
                  livemode: false
                  step: done
                  outcome: cancelled
                  locale: en
                  holdout: false
                  published: false
                  customer:
                    id: cst_vyBRXyK4QwPYf9mu
                    stripe_customer_id: null
                    external_id: sandbox_474b675f
                  subscription:
                    id: nsub_G5GU7sVoHJFgxgQc
                    plan: Pro
                    status: active
                    amount_cents: 4900
                    currency: usd
                    interval: month
                    interval_count: 1
                    current_period_end: 2026-11-09T10:00:00Z
                    cancel_at_period_end: false
                  subscriptions: []
                  reasons:
                    - key: too_expensive
                      label: It's too expensive
                      free_text: false
                    - key: missing_feature
                      label: A feature is missing
                      free_text: false
                    - key: not_using
                      label: I don't use it enough
                      free_text: false
                    - key: switching
                      label: I'm switching to another tool
                      free_text: false
                    - key: temporary
                      label: I only need a break
                      free_text: false
                    - key: project_finished
                      label: My project is finished
                      free_text: false
                    - key: other
                      label: Something else
                      free_text: true
                  offer: null
                  talk_url: null
                  texts:
                    review_ask: Would you leave a short review on {{platform}}? No reward, just your honest opinion.
                    review_cta: Leave a review
                    title: Before you go
                    reason_prompt: Why are you cancelling?
                    other_placeholder: Tell us more (optional)
                    offer_title: One last thing
                    accept: Accept the offer
                    decline: No thanks, continue
                    feedback_prompt: Anything we could have done better?
                    feedback_submit: Continue
                    confirm_title: Cancel your subscription?
                    confirm_body: You keep access until the end of the paid period.
                    cancel_now: Cancel my subscription
                    saved_title: Thanks for staying!
                    cancelled_title: Your subscription is cancelled
                    cancelled_body: You keep access until the end of the paid period.
                    offer_discount: "{{percent}}% off for {{months}} months"
                    offer_pause: Pause for {{months}} months, free
                    offer_downgrade: Switch to {{plan}}
                    offer_talk: Talk to the team
                    choose_subscription_title: Which subscription?
                    choose_subscription_body: You have several subscriptions. Choose the one you want to cancel.
                    choose_subscription_select: Continue with this one
                    cancel_this_one: Cancel this one
                    renews_on: Renews on {{date}}
                  feedback_enabled: true
                  feedback:
                    enabled: true
                    required: false
                  theme:
                    object: cancel_flow_theme
                    preset: light
                    format: modal
                    tokens:
                      bg: "#f5f6fa"
                      surface: "#ffffff"
                      text: "#1c1e4a"
                      muted: "#686b85"
                      accent: "#4637d9"
                      accent_text: "#ffffff"
                      border: "#e3e5ec"
                      danger: "#c2410c"
                      font: system-ui, sans-serif
                      font_size: 15
                      radius: 12
                      radius_control: 8
                      overlay: 0.5
                      max_width: 480
                      shadow: md
                      spacing: regular
                      border_width: 1
                      border_style: solid
                      font_scale: 1.25
                      font_weight_title: 600
                    font_url: null
                    brand: logo
                    logo_url: null
                    app_name: Account 1
                    end_screen:
                      style: celebrate
                      auto_close_seconds: 6
                    custom_css: ""
                    cancel_button_check: ok
                    classes:
                      - nc-modal
                      - nc-title
                      - nc-reason
                      - nc-offer
                      - nc-btn-primary
                      - nc-btn-cancel
                      - nc-footer
                  cancel_url_now: null
                  cancel_status: requested
                  cancel_effective_at: 2026-11-09T10:00:00Z
                  result: null
                  access_until: 2026-11-09T10:00:00Z
                  confirmation_email_sent: false
                  review: null
                  expires_at: 2026-11-05T10:00:00Z
                  created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SandboxCancelFlowRun"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: string
                subscription:
                  type: string
                  description: "Several live subscriptions: the one to play (default: the first listed)."
                reason:
                  type: string
                accept_offer:
                  type: boolean
                locale:
                  type: string
            example:
              customer: cst_vyBRXyK4QwPYf9mu
        required: true
  /v1/integration/verify:
    get:
      summary: Integration checklist
      tags:
        - Agent integration
      operationId: verifyIntegration
      description: "The 7 points of BRIEF §5.9.4, read from the account's test-mode data: pass, fail (with its fix) or unknown. Sandbox runs never count as your app's work for billing_connected and cancel_flow_opened."
      responses:
        "200":
          description: the checklist
          content:
            application/json:
              example:
                object: integration_verification
                livemode: false
                complete: false
                passed: 0
                total: 7
                checks:
                  - key: billing_connected
                    status: fail
                    detail: No billing connected and no customer received from your app yet.
                    fix: Send a customer (POST /v1/customers) and its subscription (POST /v1/subscriptions) from your backend with your test key.
                  - key: webhook_verified
                    status: fail
                    detail: No webhook endpoint in test mode.
                    fix: Create one (webhook_endpoints_create) on a route of your backend that checks NoChurn-Signature (integration_get_snippets case webhooks).
                  - key: identify_received
                    status: fail
                    detail: No attribute received yet.
                    fix: "From your backend, send signup_source and onboarding_done (and the attributes of your app) when a user signs up or changes: POST /v1/customers/{id}/attributes (integration_get_snippets case identify)."
                  - key: cancel_flow_opened
                    status: fail
                    detail: Your app has not opened the cancel flow yet (sandbox runs do not count).
                    fix: "Wire your cancel button: your backend signs a customer token and your app starts POST /v1/cancel_sessions (integration_get_snippets case signed_token; the cancel.js modal is not released yet), then click it once in test mode."
                  - key: click_to_cancel
                    status: fail
                    detail: No cancellation in test mode yet.
                    fix: Cancel once in test mode from your app (POST /v1/cancel_sessions/{id}/cancel), or play it with sandbox_run_cancel_flow.
                  - key: failed_payment_email
                    status: fail
                    detail: No failed payment in test mode yet.
                    fix: Create a sandbox customer (sandbox_create_customer), then sandbox_simulate_failed_payment.
                  - key: no_secret_in_code
                    status: unknown
                    detail: No code sent.
                    fix: "Call integration_verify with snippets: the files that call NoChurn or hold configuration (front end and back end)."
                go_live_url: null
              schema:
                $ref: "#/components/schemas/IntegrationVerification"
        "403":
          description: live mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    post:
      summary: Integration checklist, with the code to scan for secrets
      tags:
        - Agent integration
      operationId: verifyIntegrationWithCode
      description: "Same checklist; no_secret_in_code scans the files sent (secret keys nc_, sk_/rk_, whsec_, ncws_). Nothing is stored: findings only carry the file, the line and a masked preview."
      parameters: []
      responses:
        "200":
          description: the checklist
          content:
            application/json:
              example:
                object: integration_verification
                livemode: false
                complete: false
                passed: 0
                total: 7
                checks:
                  - key: billing_connected
                    status: fail
                    detail: No billing connected and no customer received from your app yet.
                    fix: Send a customer (POST /v1/customers) and its subscription (POST /v1/subscriptions) from your backend with your test key.
                  - key: webhook_verified
                    status: fail
                    detail: No webhook endpoint in test mode.
                    fix: Create one (webhook_endpoints_create) on a route of your backend that checks NoChurn-Signature (integration_get_snippets case webhooks).
                  - key: identify_received
                    status: fail
                    detail: No attribute received yet.
                    fix: "From your backend, send signup_source and onboarding_done (and the attributes of your app) when a user signs up or changes: POST /v1/customers/{id}/attributes (integration_get_snippets case identify)."
                  - key: cancel_flow_opened
                    status: fail
                    detail: Your app has not opened the cancel flow yet (sandbox runs do not count).
                    fix: "Wire your cancel button: your backend signs a customer token and your app starts POST /v1/cancel_sessions (integration_get_snippets case signed_token; the cancel.js modal is not released yet), then click it once in test mode."
                  - key: click_to_cancel
                    status: fail
                    detail: No cancellation in test mode yet.
                    fix: Cancel once in test mode from your app (POST /v1/cancel_sessions/{id}/cancel), or play it with sandbox_run_cancel_flow.
                  - key: failed_payment_email
                    status: fail
                    detail: No failed payment in test mode yet.
                    fix: Create a sandbox customer (sandbox_create_customer), then sandbox_simulate_failed_payment.
                  - key: no_secret_in_code
                    status: fail
                    detail: "Secrets found: src/app.js:1 nochurn_live_secret_key (nc_live_…ZZ)."
                    fix: Move them to environment variables read by your backend only (e.g. process.env.NOCHURN_API_KEY), never in the app or the front end; revoke a key that was committed.
                go_live_url: null
              schema:
                $ref: "#/components/schemas/IntegrationVerification"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - snippets
              properties:
                snippets:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    required:
                      - content
                    properties:
                      path:
                        type: string
                      content:
                        type: string
            example:
              snippets:
                - path: src/app.js
                  content: const k = 'nc_live_EXAMPLE00000000000000000'
        required: true
  /v1/integration/detect:
    post:
      summary: Detect the project's platforms and plan the integration
      tags:
        - Agent integration
      operationId: detectProject
      description: The agent sends file paths, dependency names and the dependency manifests; nothing is stored. Flags App Store / Google Play subscriptions, which NoChurn cannot cancel server side.
      parameters: []
      responses:
        "200":
          description: platforms and plan
          content:
            application/json:
              example:
                object: project_detection
                platforms:
                  - key: nextjs
                    name: Next.js
                    kind: web
                    evidence:
                      - dependency next
                      - file next.config.mjs
                backend: nextjs
                frontend: nextjs
                mobile: []
                billing:
                  stripe: true
                  store_subscriptions:
                    detected: false
                    stores: []
                    evidence: []
                    note: null
                plan:
                  steps:
                    - key: setup
                      title: Read what is installed and what is missing
                      status: available
                      tools:
                        - account_get_setup
                      snippet_cases: []
                    - key: billing
                      title: Connect Stripe in test mode (a human clicks the link)
                      status: available
                      tools:
                        - account_connect_stripe_link
                      snippet_cases: []
                    - key: keys
                      title: Create a test secret key for the backend (env NOCHURN_API_KEY) and get the publishable key
                      status: available
                      tools:
                        - api_keys_create_test
                        - publishable_key_get
                      snippet_cases: []
                    - key: identify
                      title: Send customers and their attributes (identify) and usage events (track)
                      status: available
                      tools:
                        - attributes_define
                        - integration_get_snippets
                      snippet_cases:
                        - identify
                        - track
                      snippet_platform: nextjs
                    - key: webhooks
                      title: Receive NoChurn's webhooks on a route that checks the signature
                      status: available
                      tools:
                        - webhook_endpoints_create
                        - integration_get_snippets
                        - webhook_endpoints_send_test
                      snippet_cases:
                        - webhooks
                      snippet_platform: nextjs
                    - key: cancel_flow
                      title: Configure the cancel flow (reasons, offers, theme from the project's colours) and publish it in test mode
                      status: available
                      tools:
                        - cancel_flow_get
                        - cancel_flow_update_draft
                        - cancel_flow_set_theme
                        - flow_publish_test
                        - cancel_flow_preview_url
                      snippet_cases: []
                    - key: customer_token
                      title: Sign the customer token on your backend (widget secret, env NOCHURN_WIDGET_SECRET)
                      status: available
                      tools:
                        - integration_get_snippets
                      snippet_cases:
                        - signed_token
                      snippet_platform: nextjs
                    - key: cancel_button_web
                      title: "Wire your cancel button: headless cancel flow (the cancel.js modal is not released yet)"
                      status: available
                      tools:
                        - integration_get_snippets
                      snippet_cases:
                        - headless_cancel
                      snippet_platform: web
                    - key: access_check
                      title: "Block your app in the backend while a payment fails (GET /v1/customers/{id}/access): the widget's wall alone can be bypassed"
                      status: available
                      tools:
                        - integration_get_snippets
                      snippet_cases:
                        - access_check
                      snippet_platform: nextjs
                    - key: sandbox
                      title: Play the scenarios end to end in test mode
                      status: available
                      tools:
                        - sandbox_create_customer
                        - sandbox_simulate_failed_payment
                        - sandbox_simulate_card_removed
                        - sandbox_run_cancel_flow
                      snippet_cases: []
                    - key: verify
                      title: Run the checklist until everything is green, sending the code you changed
                      status: available
                      tools:
                        - integration_verify
                      snippet_cases: []
                    - key: go_live
                      title: "Hand over to a human: going live (live keys, live publication) is approved in the dashboard"
                      status: available
                      tools:
                        - request_go_live
                      snippet_cases: []
                  warnings: []
              schema:
                $ref: "#/components/schemas/ProjectDetection"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items:
                    type: string
                  maxItems: 5000
                dependencies:
                  type: array
                  items:
                    type: string
                manifests:
                  type: array
                  items:
                    type: object
                    required:
                      - path
                      - content
                    properties:
                      path:
                        type: string
                      content:
                        type: string
            example:
              files:
                - next.config.mjs
              manifests:
                - path: package.json
                  content: '{"dependencies":{"next":"15","stripe":"17"}}'
        required: true
  /v1/integration/snippets:
    get:
      summary: Tested code for a platform and a case
      tags:
        - Agent integration
      operationId: getIntegrationSnippets
      description: Only code tested against this API, versioned. Cases without a released SDK (modal, native apps) answer not_available_yet.
      parameters:
        - name: platform
          in: query
          required: false
          schema:
            type: string
            example: nextjs
        - name: case
          in: query
          required: false
          schema:
            type: string
            enum:
              - identify
              - track
              - webhooks
              - signed_token
              - billing_by_api
              - access_check
              - headless_cancel
              - modal
              - card_update
      responses:
        "200":
          description: the snippets
          content:
            application/json:
              example:
                object: integration_snippets
                version: 2026-11-06.2
                platform: rails
                language: ruby
                api_base_url: https://api.nochurn.sh
                snippets:
                  - case: webhooks
                    status: available
                    title: Verify NoChurn's webhook signature
                    language: ruby
                    label: Ruby (Net::HTTP, OpenSSL)
                    syntax: ruby
                    runs_on: server
                    insert: A new POST route of your backend (e.g. /webhooks/nochurn), registered with webhook_endpoints_create.
                    env:
                      - NOCHURN_WEBHOOK_SECRET
                    file: webhooks/ruby.rb
                    code: |
                      # NoChurn: verify a webhook (NoChurn-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.<raw body>">) in Ruby.
                      # Env: NOCHURN_WEBHOOK_SECRET (whsec_…). Use the raw body (Rails: request.raw_post).
                      require "openssl"

                      def verify_nochurn_signature(raw_body, header, secret = ENV.fetch("NOCHURN_WEBHOOK_SECRET"), tolerance: 300)
                        parts = header.to_s.split(",").to_h { |part| part.split("=", 2) }
                        t = Integer(parts["t"].to_s, exception: false)
                        return false if t.nil? || (Time.now.to_i - t).abs > tolerance || parts["v1"].nil?

                        expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts['t']}.#{raw_body}")
                        expected.bytesize == parts["v1"].bytesize && OpenSSL.fixed_length_secure_compare(expected, parts["v1"])
                      end

                      # Rails example (skip CSRF on this API controller):
                      #   def create
                      #     return head(:bad_request) unless verify_nochurn_signature(request.raw_post, request.headers["NoChurn-Signature"])
                      #     event = JSON.parse(request.raw_post)
                      #     head :ok
                      #   end
              schema:
                $ref: "#/components/schemas/IntegrationSnippets"
        "400":
          description: unknown platform
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/subscriptions:
    post:
      summary: Send a subscription (billing by API)
      tags:
        - Billing
      operationId: createSubscription
      description: "Idempotent on your external_id: posting it again updates the subscription."
      parameters: []
      responses:
        "201":
          description: created with its customer
          content:
            application/json:
              example:
                object: subscription
                id: nsub_hd7rffvqSB3fF8LR
                customer: cst_XBTW4dWsGxyLYcgT
                stripe_subscription_id: null
                external_id: sub_42
                livemode: false
                status: active
                plan: Pro
                amount_cents: 4900
                currency: eur
                interval: month
                interval_count: 1
                mrr_cents: 4900
                current_period_end: null
                cancel_at_period_end: false
                canceled_at: null
                paused_until: null
                trial_end: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Subscription"
        "409":
          description: the account's billing comes from Stripe
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - external_id
                - customer
                - amount
                - currency
                - interval
              properties:
                external_id:
                  type: string
                  example: sub_42
                customer:
                  oneOf:
                    - type: string
                    - type: object
                      properties:
                        external_id:
                          type: string
                        email:
                          type: string
                        name:
                          type: string
                plan:
                  type: string
                amount:
                  type: integer
                  example: 4900
                currency:
                  type: string
                  example: eur
                interval:
                  type: string
                  enum:
                    - day
                    - week
                    - month
                    - year
                status:
                  type: string
                  enum:
                    - incomplete
                    - incomplete_expired
                    - trialing
                    - active
                    - past_due
                    - canceled
                    - unpaid
                    - paused
                current_period_end:
                  type: string
                  format: date-time
            example:
              external_id: sub_42
              customer:
                external_id: user_42
                email: lena@globex.io
              plan: Pro
              amount: 4900
              currency: eur
              interval: month
        required: true
    get:
      summary: List subscriptions, optionally for one customer
      tags:
        - Subscriptions
      operationId: listSubscriptions
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
          description: cst_…, cus_… or your id.
        - name: stripe_customer
          in: query
          required: false
          schema:
            type: string
        - name: external_id
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: list
          content:
            application/json:
              example:
                object: list
                data:
                  - object: subscription
                    id: nsub_g7DG7iWyLNMTChNe
                    customer: cst_UJ48q25bPE3jdT3u
                    stripe_subscription_id: sub_1
                    external_id: null
                    livemode: false
                    status: active
                    plan: Pro
                    amount_cents: 4900
                    currency: eur
                    interval: month
                    interval_count: 1
                    mrr_cents: 4900
                    current_period_end: 2026-11-24T10:00:00.000Z
                    cancel_at_period_end: false
                    canceled_at: null
                    paused_until: null
                    trial_end: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/SubscriptionList"
        "404":
          description: unknown customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/payments:
    post:
      summary: Send a payment (billing by API)
      tags:
        - Billing
      operationId: createPayment
      description: "status failed opens recovery: NoChurn plans retries and requests each one with billing.retry_requested. status succeeded pays the invoice and ends recovery. Idempotent on your external_id."
      parameters: []
      responses:
        "201":
          description: a failed payment opens recovery
          content:
            application/json:
              example:
                object: payment
                id: npay_TBSmR2L4qghkw8q1
                livemode: false
                customer: cst_xWuFumaBvkQ9HWYw
                subscription: nsub_Zv2tbZpFo5YxCaPy
                invoice: ninv_JKntY71SKsXEe55e
                external_id: pay_9
                stripe_charge_id: null
                status: failed
                amount_cents: 4900
                currency: eur
                decline_code: do_not_honor
                failure_code: null
                occurred_at: 2026-11-04T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Payment"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - external_id
                - customer
                - amount
                - currency
                - status
              properties:
                external_id:
                  type: string
                  example: pay_9
                customer:
                  type: string
                  example: user_42
                subscription:
                  type: string
                  example: sub_42
                invoice:
                  type: string
                  example: inv_2026_11
                amount:
                  type: integer
                  example: 4900
                currency:
                  type: string
                  example: eur
                status:
                  type: string
                  enum:
                    - failed
                    - succeeded
                decline_code:
                  type: string
                  example: insufficient_funds
                occurred_at:
                  type: string
                  format: date-time
                invoice_url:
                  type: string
                  description: Where your customer pays this invoice.
            example:
              external_id: pay_9
              customer: user_42
              subscription: sub_42
              invoice: inv_1
              amount: 4900
              currency: eur
              status: failed
              decline_code: do_not_honor
        required: true
    get:
      summary: Payments (Stripe and API)
      tags:
        - Billing
      operationId: listPayments
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the list
          content:
            application/json:
              example:
                object: list
                data: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/PaymentList"
      x-audience: public
  /v1/subscriptions/{id}:
    parameters:
      - name: id
        in: path
        description: nsub_… or your external_id
        required: true
        schema:
          type: string
    patch:
      summary: Change a subscription sent by API (subscription.cancelled)
      tags:
        - Billing
      operationId: updateSubscription
      parameters: []
      responses:
        "200":
          description: canceled
          content:
            application/json:
              example:
                object: subscription
                id: nsub_Wf9KF5qNWWFiJ6N6
                customer: cst_5SJ8JmubQCqFJzQw
                stripe_subscription_id: null
                external_id: sub_42
                livemode: false
                status: canceled
                plan: null
                amount_cents: 4900
                currency: eur
                interval: month
                interval_count: 1
                mrr_cents: 0
                current_period_end: null
                cancel_at_period_end: false
                canceled_at: 2026-11-04T10:00:00.000Z
                paused_until: null
                trial_end: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Subscription"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - incomplete
                    - incomplete_expired
                    - trialing
                    - active
                    - past_due
                    - canceled
                    - unpaid
                    - paused
                plan:
                  type: string
                amount:
                  type: integer
                current_period_end:
                  type: string
                  format: date-time
                cancel_at_period_end:
                  type: boolean
            example:
              status: canceled
        required: true
  /v1/api_keys:
    get:
      summary: List the API keys of the current mode (live key or NoChurn-Mode header)
      tags:
        - API keys
      operationId: listApiKeys
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
          description: "Cursor: the next_cursor of the previous page."
      responses:
        "200":
          description: list of API keys (secret tokens are never included)
          content:
            application/json:
              example:
                object: list
                data:
                  - object: api_key
                    id: key_KCvgEydsuK5U3G11
                    name: Key 1
                    kind: secret
                    mode: live
                    access: full
                    permissions:
                      account: write
                      api_keys: write
                      team: write
                      events: write
                      customers: write
                      billing: write
                      stripe_connections: write
                      logs: write
                      flows: write
                      email_domains: write
                      messages: write
                      webhooks: write
                      cancel_sessions: write
                      reviews: write
                      dashboard: write
                      insights: write
                      integrations: write
                      queries: write
                      disputes: write
                      sandbox: write
                    last4: G91q
                    allowed_origins: null
                    token: null
                    first_used_at: null
                    last_used_at: null
                    revoked_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                  - object: api_key
                    id: key_5MfRNbFWjVjA9Pwj
                    name: Key 4
                    kind: secret
                    mode: live
                    access: full
                    permissions:
                      account: write
                      api_keys: write
                      team: write
                      events: write
                      customers: write
                      billing: write
                      stripe_connections: write
                      logs: write
                      flows: write
                      email_domains: write
                      messages: write
                      webhooks: write
                      cancel_sessions: write
                      reviews: write
                      dashboard: write
                      insights: write
                      integrations: write
                      queries: write
                      disputes: write
                      sandbox: write
                    last4: hLa2
                    allowed_origins: null
                    token: null
                    first_used_at: 2026-11-04T10:00:00.000Z
                    last_used_at: 2026-11-04T10:00:00.000Z
                    revoked_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/ApiKeyList"
        "400":
          description: invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: missing, unknown or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    post:
      summary: Create an API key (the token is returned once)
      tags:
        - API keys
      operationId: createApiKey
      parameters: []
      responses:
        "201":
          description: API key created, the token is returned once
          content:
            application/json:
              example:
                object: api_key
                id: key_GyLvx41FGQQBjCYX
                name: CI
                kind: secret
                mode: test
                access: full
                permissions:
                  account: write
                  api_keys: write
                  team: write
                  events: write
                  customers: write
                  billing: write
                  stripe_connections: write
                  logs: write
                  flows: write
                  email_domains: write
                  messages: write
                  webhooks: write
                  cancel_sessions: write
                  reviews: write
                  dashboard: write
                  insights: write
                  integrations: write
                  queries: write
                  disputes: write
                  sandbox: write
                last4: FLub
                allowed_origins: null
                token: nc_test_EXAMPLE00000000000000000
                first_used_at: null
                last_used_at: null
                revoked_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/ApiKey"
        "400":
          description: invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: a test key cannot create a live key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                mode:
                  type: string
                  enum:
                    - live
                    - test
                  description: Defaults to the current mode. A test key can only create test keys.
                kind:
                  type: string
                  enum:
                    - secret
                    - publishable
                  description: "Default: secret. Publishable keys (pk_) are for the cancel widget."
                access:
                  type: string
                  enum:
                    - full
                    - restricted
                  description: "Default: full. A key never creates a key more powerful than itself."
                allowed_origins:
                  type: array
                  items:
                    type: string
                    example: https://app.acme.com
                  description: "Publishable keys only: exact origins allowed to call the public endpoints. Left out, they default to https://<domain> and https://app.<domain> from the email of the user creating the key (else the account owner), unless it is a free or disposable mail domain. A list, even empty, is kept as given."
                permissions:
                  type: object
                  additionalProperties: false
                  description: "Restricted keys: level per resource (missing = none)."
                  properties:
                    account:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    api_keys:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    team:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    events:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    customers:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    billing:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    stripe_connections:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    logs:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    flows:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    email_domains:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    messages:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    webhooks:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    cancel_sessions:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    reviews:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    dashboard:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    insights:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    integrations:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    queries:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    disputes:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    sandbox:
                      type: string
                      enum:
                        - none
                        - read
                        - write
            example:
              name: CI
              mode: test
        required: true
  /v1/api_keys/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Rename a key or change its access
      tags:
        - API keys
      operationId: updateApiKey
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: api_key
                id: key_mmRMLWbYwsDkVuTW
                name: CI (read only)
                kind: secret
                mode: test
                access: restricted
                permissions:
                  account: none
                  api_keys: none
                  team: none
                  events: none
                  customers: read
                  billing: none
                  stripe_connections: none
                  logs: none
                  flows: none
                  email_domains: none
                  messages: none
                  webhooks: none
                  cancel_sessions: none
                  reviews: none
                  dashboard: none
                  insights: none
                  integrations: none
                  queries: none
                  disputes: none
                  sandbox: none
                last4: zNiY
                allowed_origins: null
                token: null
                first_used_at: null
                last_used_at: null
                revoked_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/ApiKey"
        "403":
          description: more rights than the calling key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                access:
                  type: string
                  enum:
                    - full
                    - restricted
                permissions:
                  type: object
                  additionalProperties: false
                  properties:
                    account:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    api_keys:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    team:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    events:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    customers:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    billing:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    stripe_connections:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    logs:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    flows:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    email_domains:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    messages:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    webhooks:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    cancel_sessions:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    reviews:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    dashboard:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    insights:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    integrations:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    queries:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    disputes:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                    sandbox:
                      type: string
                      enum:
                        - none
                        - read
                        - write
                allowed_origins:
                  type: array
                  items:
                    type: string
                    example: https://app.acme.com
                  description: "Publishable keys only: origins allowed to call the public endpoints."
            example:
              name: CI (read only)
              access: restricted
              permissions:
                customers: read
        required: true
    delete:
      summary: Revoke an API key
      tags:
        - API keys
      operationId: revokeApiKey
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              example:
                object: api_key
                id: key_VYxpFgpYz8anfPXj
                name: Key 1
                kind: secret
                mode: test
                access: full
                permissions:
                  account: write
                  api_keys: write
                  team: write
                  events: write
                  customers: write
                  billing: write
                  stripe_connections: write
                  logs: write
                  flows: write
                  email_domains: write
                  messages: write
                  webhooks: write
                  cancel_sessions: write
                  reviews: write
                  dashboard: write
                  insights: write
                  integrations: write
                  queries: write
                  disputes: write
                  sandbox: write
                last4: oRgd
                allowed_origins: null
                token: null
                first_used_at: null
                last_used_at: null
                revoked_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/ApiKey"
        "403":
          description: a test key cannot revoke a live key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: key of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: already revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/billing_connections:
    get:
      summary: Connected billing providers
      tags:
        - Integrations
      operationId: listBillingConnections
      description: Every provider with its capability matrix (what NoChurn can do with it). Stripe keeps /v1/stripe_connections.
      responses:
        "200":
          description: with capabilities
          content:
            application/json:
              example:
                object: list
                data:
                  - object: billing_connection
                    id: scn_LVV5Gkx9djjhXDGN
                    provider: stripe
                    livemode: false
                    status: active
                    external_account_id: acct_test1
                    business_name: null
                    capabilities:
                      retries: nochurn
                      card_update: hosted_page
                      offers:
                        - coupon
                        - pause
                        - downgrade
                      cancel_via_api: true
                      disputes: evidence_api
                      fraud_warnings: true
                      webhooks: true
                      backfill: true
                    available: true
                    webhook_url: null
                    backfill:
                      status: done
                      progress: {}
                      started_at: null
                      finished_at: null
                      error: null
                    webhook_secret_set: true
                    settings: {}
                    internal_id:
                      field: null
                      defaults:
                        - metadata.user_id
                      fields:
                        - metadata.<name>
                    last_event_at: null
                    error: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/BillingConnectionList"
      x-audience: public
    post:
      summary: Connect a provider with API keys
      tags:
        - Integrations
      operationId: createBillingConnection
      description: "Providers arrive milestone by milestone: until then, provider_not_available."
      parameters: []
      responses:
        "201":
          description: "connected (App Store sandbox): Apple keeps payment, retries and cancellation"
          content:
            application/json:
              example:
                object: billing_connection
                id: bcn_WLPj135TCf4cf9uS
                provider: chargebee
                livemode: false
                status: active
                external_account_id: acme-test
                business_name: acme-test
                capabilities:
                  retries: provider
                  card_update: provider_portal
                  offers:
                    - coupon
                    - pause
                  cancel_via_api: true
                  disputes: provider_handles
                  fraud_warnings: false
                  webhooks: true
                  backfill: true
                available: true
                webhook_url: https://api.nochurn.sh/webhooks/chargebee/-QEPYRNIzgjDa6SdZxc02GO3oW9oHifu
                backfill:
                  status: pending
                  progress: {}
                  started_at: null
                  finished_at: null
                  error: null
                webhook_secret_set: true
                settings: {}
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                    - cf_user_id
                  fields:
                    - metadata.<name>
                    - cf_<name>
                    - id
                last_event_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                webhook_setup:
                  basic_auth_username: nochurn
                  basic_auth_password: EXAMPLE00000000000000000
                  api_version: v2
              schema:
                $ref: "#/components/schemas/BillingConnection"
        "400":
          description: unknown provider
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - provider
              properties:
                provider:
                  type: string
                  enum:
                    - stripe
                    - chargebee
                    - recurly
                    - maxio
                    - zuora
                    - paddle
                    - lemon_squeezy
                    - polar
                    - braintree
                    - mollie
                    - gocardless
                    - razorpay
                    - app_store
                    - google_play
                    - custom
                credentials:
                  type: object
                  additionalProperties:
                    type: string
                  description: "The fields listed by GET /v1/billing_providers (e.g. site, api_key, region, webhook_secret; App Store: private_key, key_id, issuer_id, bundle_id; Google Play: service_account_json, package_name)."
                settings:
                  type: object
                  description: 'Stores only: refund_review { customer_consented, sample_content_provided } (booleans); Google Play: push_audience, push_service_account_email ("" clears).'
                  properties:
                    refund_review:
                      type: object
                      properties:
                        customer_consented:
                          type: boolean
                        sample_content_provided:
                          type: boolean
                    push_audience:
                      type: string
                    push_service_account_email:
                      type: string
            example:
              provider: chargebee
              credentials:
                site: acme-test
                api_key: test_x
        required: true
  /v1/billing_connections/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    get:
      summary: A billing connection
      tags:
        - Integrations
      operationId: getBillingConnection
      responses:
        "200":
          description: the connection
          content:
            application/json:
              example:
                object: billing_connection
                id: scn_4XXipZYv1tnXU99X
                provider: stripe
                livemode: false
                status: active
                external_account_id: acct_test1
                business_name: null
                capabilities:
                  retries: nochurn
                  card_update: hosted_page
                  offers:
                    - coupon
                    - pause
                    - downgrade
                  cancel_via_api: true
                  disputes: evidence_api
                  fraud_warnings: true
                  webhooks: true
                  backfill: true
                available: true
                webhook_url: null
                backfill:
                  status: done
                  progress: {}
                  started_at: null
                  finished_at: null
                  error: null
                webhook_secret_set: true
                settings: {}
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                  fields:
                    - metadata.<name>
                last_event_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/BillingConnection"
      x-audience: public
    patch:
      summary: Change capabilities, the Stripe retry mode or where your user id is read
      tags:
        - Integrations
      operationId: updateBillingConnection
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: billing_connection
                id: scn_qHHubuDxvBUkk4ZN
                provider: stripe
                livemode: false
                status: active
                external_account_id: acct_test1
                business_name: null
                capabilities:
                  retries: provider
                  card_update: hosted_page
                  offers:
                    - coupon
                    - pause
                    - downgrade
                  cancel_via_api: true
                  disputes: evidence_api
                  fraud_warnings: true
                  webhooks: true
                  backfill: true
                available: true
                webhook_url: null
                backfill:
                  status: done
                  progress: {}
                  started_at: null
                  finished_at: null
                  error: null
                webhook_secret_set: true
                settings: {}
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                  fields:
                    - metadata.<name>
                last_event_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/BillingConnection"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                capabilities:
                  type: object
                  description: Only the keys sent change; locked ones are refused (GET /v1/billing_providers).
                  properties:
                    retries:
                      type: string
                      enum:
                        - nochurn
                        - provider
                        - none
                    card_update:
                      type: string
                      enum:
                        - hosted_page
                        - provider_portal
                        - none
                    offers:
                      type: array
                      items:
                        type: string
                        enum:
                          - coupon
                          - pause
                          - downgrade
                    cancel_via_api:
                      type: boolean
                smart_retries_mode:
                  type: string
                  enum:
                    - stripe
                    - nochurn
                internal_id_field:
                  type: string
                  nullable: true
                  example: custom_fields.user_id
                  description: Owner / admin. Where this tool keeps your app's id for a customer (a field or one key inside one, see internal_id.fields); null or "" = the tool's defaults. Applies to what NoChurn reads from now on.
                webhook_secret:
                  type: string
                  description: "Write only: the secret of the webhook endpoint created on webhook_url (Recurly, Paddle, GoCardless, Razorpay)."
                settings:
                  type: object
                  description: 'Stores only: refund_review { customer_consented, sample_content_provided } (booleans); Google Play: push_audience, push_service_account_email ("" clears).'
                  properties:
                    refund_review:
                      type: object
                      properties:
                        customer_consented:
                          type: boolean
                        sample_content_provided:
                          type: boolean
                    push_audience:
                      type: string
                    push_service_account_email:
                      type: string
            example:
              smart_retries_mode: stripe
        required: true
    delete:
      summary: Disconnect (credentials erased; synced data stays)
      tags:
        - Integrations
      operationId: deleteBillingConnection
      responses:
        "200":
          description: revoked
          content:
            application/json:
              example:
                object: billing_connection
                id: bcn_G5sqaQTvcqqq3YGa
                provider: chargebee
                livemode: false
                status: revoked
                external_account_id: acme-test
                business_name: null
                capabilities:
                  retries: provider
                  card_update: provider_portal
                  offers:
                    - coupon
                    - pause
                  cancel_via_api: true
                  disputes: provider_handles
                  fraud_warnings: false
                  webhooks: true
                  backfill: true
                available: true
                webhook_url: null
                backfill:
                  status: done
                  progress: {}
                  started_at: null
                  finished_at: null
                  error: null
                webhook_secret_set: false
                settings: {}
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                    - cf_user_id
                  fields:
                    - metadata.<name>
                    - cf_<name>
                    - id
                last_event_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/BillingConnection"
      x-audience: public
  /v1/billing_connections/{id}/backfill:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Import again (12 months)
      tags:
        - Integrations
      operationId: backfillBillingConnection
      responses:
        "202":
          description: queued
          content:
            application/json:
              example:
                object: billing_connection
                id: bcn_bd2nxUWHSGoShYqJ
                provider: chargebee
                livemode: false
                status: active
                external_account_id: acme-test
                business_name: null
                capabilities:
                  retries: provider
                  card_update: provider_portal
                  offers:
                    - coupon
                    - pause
                  cancel_via_api: true
                  disputes: provider_handles
                  fraud_warnings: false
                  webhooks: true
                  backfill: true
                available: true
                webhook_url: https://api.nochurn.sh/webhooks/chargebee/tok_cb2
                backfill:
                  status: pending
                  progress: {}
                  started_at: null
                  finished_at: null
                  error: null
                webhook_secret_set: false
                settings: {}
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                    - cf_user_id
                  fields:
                    - metadata.<name>
                    - cf_<name>
                    - id
                last_event_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/BillingConnection"
      x-audience: public
  /v1/billing_providers:
    get:
      summary: Billing providers (supported and coming)
      tags:
        - Integrations
      operationId: listBillingProviders
      responses:
        "200":
          description: every provider, available or not
          content:
            application/json:
              example:
                object: list
                data:
                  - object: billing_provider
                    provider: stripe
                    family: billing
                    available: true
                    connect: oauth
                    credentials: []
                    capabilities:
                      retries: provider
                      card_update: hosted_page
                      offers:
                        - coupon
                        - pause
                        - downgrade
                      cancel_via_api: true
                      disputes: evidence_api
                      fraud_warnings: true
                      webhooks: true
                      backfill: true
                    locked: []
                    webhook_secret: none
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/stripe/
                  - object: billing_provider
                    provider: chargebee
                    family: billing
                    available: true
                    connect: credentials
                    credentials:
                      - name: site
                        secret: false
                        hint: "The part before .chargebee.com (test site: <name>-test)"
                      - name: api_key
                        secret: true
                        hint: Settings › Configure Chargebee › API keys (full access)
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - coupon
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked: []
                    webhook_secret: generated
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/chargebee/
                  - object: billing_provider
                    provider: recurly
                    family: billing
                    available: true
                    connect: credentials
                    credentials:
                      - name: api_key
                        secret: true
                        hint: Integrations › API credentials (private key)
                      - name: region
                        secret: false
                        hint: us or eu
                        options:
                          - us
                          - eu
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - coupon
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked: []
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/recurly/
                  - object: billing_provider
                    provider: maxio
                    family: billing
                    available: true
                    connect: credentials
                    credentials:
                      - name: subdomain
                        secret: false
                        hint: The part before .chargify.com, or the full address of an EU site (acme.ebilling.maxio.com). A site in test mode in Test mode
                      - name: api_key
                        secret: true
                        hint: Config › Integrations › API keys
                      - name: site_shared_key
                        secret: true
                        hint: "Config › Settings › Site shared key: signs the webhooks"
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - coupon
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked: []
                    webhook_secret: none
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/maxio/
                  - object: billing_provider
                    provider: zuora
                    family: billing
                    available: true
                    connect: credentials
                    credentials:
                      - name: environment
                        secret: false
                        hint: Where your tenant lives (sandbox or test in Test mode)
                        options:
                          - sandbox
                          - test
                          - na_sandbox
                          - eu_sandbox
                          - eu_test
                          - ap_test
                          - production
                          - na_production
                          - eu_production
                          - ap_production
                      - name: client_id
                        secret: false
                        hint: Settings › Administration › Manage users › OAuth client (API user with read and subscription rights)
                      - name: client_secret
                        secret: true
                        hint: Shown once when the OAuth client is created
                    capabilities:
                      retries: provider
                      card_update: none
                      offers:
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: false
                      backfill: true
                    locked:
                      - card_update
                    webhook_secret: none
                    hint: "NoChurn reads your Zuora tenant every 10 minutes (accounts, subscriptions, invoices, payments): no callout to configure. Retries and the payment page stay with Zuora."
                    docs_url: https://nochurn.sh/docs/integrations/zuora/
                  - object: billing_provider
                    provider: paddle
                    family: merchant_of_record
                    available: true
                    connect: credentials
                    credentials:
                      - name: api_key
                        secret: true
                        hint: Developer tools › Authentication (sandbox key in Test mode), with the Customer portal session permission (card update links)
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - coupon
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - disputes
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/paddle/
                  - object: billing_provider
                    provider: lemon_squeezy
                    family: merchant_of_record
                    available: true
                    connect: credentials
                    credentials:
                      - name: api_key
                        secret: true
                        hint: Settings › API (a test mode key in Test mode)
                      - name: store_id
                        secret: false
                        hint: "Settings › Stores: the store's number"
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - disputes
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/lemon-squeezy/
                  - object: billing_provider
                    provider: polar
                    family: merchant_of_record
                    available: true
                    connect: credentials
                    credentials:
                      - name: access_token
                        secret: true
                        hint: Settings › Developers › Organization access token (a sandbox.polar.sh token in Test mode)
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - coupon
                        - pause
                      cancel_via_api: true
                      disputes: provider_handles
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - disputes
                    webhook_secret: pasted
                    hint: Create a webhook endpoint in Polar › Settings › Webhooks with webhook_url, format Raw and API version 2026-10 (the version NoChurn reads), then paste its secret (whsec_…) here.
                    docs_url: https://nochurn.sh/docs/integrations/polar/
                  - object: billing_provider
                    provider: braintree
                    family: payments
                    available: true
                    connect: credentials
                    credentials:
                      - name: merchant_id
                        secret: false
                        hint: Settings › Business (sandbox account in Test mode)
                      - name: public_key
                        secret: false
                        hint: Settings › API › API keys
                      - name: private_key
                        secret: true
                        hint: Settings › API › API keys (View)
                    capabilities:
                      retries: provider
                      card_update: none
                      offers:
                        - coupon
                      cancel_via_api: true
                      disputes: evidence_api
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - card_update
                    webhook_secret: none
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/braintree/
                  - object: billing_provider
                    provider: mollie
                    family: payments
                    available: true
                    connect: credentials
                    credentials:
                      - name: api_key
                        secret: true
                        hint: Developers › API keys (test_… in Test mode, live_… in Live mode)
                    capabilities:
                      retries: nochurn
                      card_update: hosted_page
                      offers: []
                      cancel_via_api: true
                      disputes: none
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - disputes
                    webhook_secret: none
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/mollie/
                  - object: billing_provider
                    provider: gocardless
                    family: payments
                    available: true
                    connect: credentials
                    credentials:
                      - name: access_token
                        secret: true
                        hint: Developers › Create › Access token, read-write (sandbox token in Test mode)
                    capabilities:
                      retries: nochurn
                      card_update: hosted_page
                      offers:
                        - pause
                      cancel_via_api: true
                      disputes: none
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - disputes
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/gocardless/
                  - object: billing_provider
                    provider: razorpay
                    family: payments
                    available: true
                    connect: credentials
                    credentials:
                      - name: key_id
                        secret: false
                        hint: Account & Settings › API keys (rzp_test_… in Test mode)
                      - name: key_secret
                        secret: true
                        hint: Shown once when the key is generated
                    capabilities:
                      retries: provider
                      card_update: provider_portal
                      offers:
                        - pause
                      cancel_via_api: true
                      disputes: evidence_api
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - offers
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/razorpay/
                  - object: billing_provider
                    provider: app_store
                    family: stores
                    available: true
                    connect: credentials
                    credentials:
                      - name: private_key
                        secret: true
                        hint: The .p8 file of an In-App Purchase key (App Store Connect › Users and Access › Integrations › In-App Purchase), pasted whole
                      - name: key_id
                        secret: false
                        hint: The key's ID, next to it in App Store Connect
                      - name: issuer_id
                        secret: false
                        hint: Issuer ID, at the top of the same page
                      - name: bundle_id
                        secret: false
                        hint: Your app's bundle ID (e.g. com.acme.app)
                    capabilities:
                      retries: none
                      card_update: none
                      offers: []
                      cancel_via_api: false
                      disputes: refund_review
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - card_update
                      - disputes
                    webhook_secret: none
                    hint: "Apple shares no email: customers are linked only when your app sets an appAccountToken at purchase and sends it to NoChurn (as the external_id, or the app_account_token attribute of the customer). Without it, events stay unlinked. Paste webhook_url as the App Store Server Notifications V2 URL (sandbox URL in Test mode)."
                    docs_url: https://nochurn.sh/docs/integrations/app-store/
                  - object: billing_provider
                    provider: google_play
                    family: stores
                    available: true
                    connect: credentials
                    credentials:
                      - name: service_account_json
                        secret: true
                        hint: JSON key of a Google Cloud service account invited in Play Console › Users and permissions (view financial data, manage orders and subscriptions)
                      - name: package_name
                        secret: false
                        hint: Your app's package name (e.g. com.acme.app)
                    capabilities:
                      retries: none
                      card_update: none
                      offers: []
                      cancel_via_api: true
                      disputes: refund_review
                      fraud_warnings: false
                      webhooks: true
                      backfill: true
                    locked:
                      - retries
                      - card_update
                      - disputes
                    webhook_secret: none
                    hint: Customers are linked only when your app sets obfuscatedAccountId at purchase and sends it to NoChurn (as the external_id, or the obfuscated_account_id attribute of the customer). Without it, events stay unlinked. Create a Pub/Sub push subscription to webhook_url on the topic of your real-time developer notifications. Test mode applies license testers' purchases only.
                    docs_url: https://nochurn.sh/docs/integrations/google-play/
                  - object: billing_provider
                    provider: custom
                    family: custom
                    available: true
                    connect: credentials
                    credentials:
                      - name: name
                        secret: false
                        hint: The name of your billing system, as shown in NoChurn
                      - name: signature_mode
                        secret: false
                        hint: How your billing system signs its webhooks
                        options:
                          - hmac_sha256
                          - header_secret
                      - name: signature_header
                        secret: false
                        hint: The header carrying the signature or the secret (e.g. X-Signature)
                      - name: signature_encoding
                        secret: false
                        hint: "HMAC only: how the signature is written"
                        options:
                          - hex
                          - base64
                    capabilities:
                      retries: none
                      card_update: none
                      offers: []
                      cancel_via_api: false
                      disputes: none
                      fraud_warnings: false
                      webhooks: true
                      backfill: false
                    locked:
                      - retries
                      - card_update
                      - offers
                      - cancel_via_api
                      - disputes
                      - fraud_warnings
                      - webhooks
                      - backfill
                    webhook_secret: pasted
                    hint: null
                    docs_url: https://nochurn.sh/docs/integrations/custom/
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/BillingProviderList"
      x-audience: public
  /v1/billing_connections/{billing_connection_id}/mapping:
    parameters:
      - name: billing_connection_id
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Mapping of a custom billing connection, tested on its sample
      tags:
        - Integrations
      operationId: getBillingMapping
      responses:
        "200":
          description: rules, state and the last webhooks with their result
          content:
            application/json:
              example:
                object: billing_mapping
                billing_connection: bcn_pEFhqdfNzf8kNZu4
                active: false
                activated_at: null
                rules: []
                types:
                  - customer.upserted
                  - subscription.created
                  - subscription.updated
                  - subscription.paused
                  - subscription.resumed
                  - subscription.canceled
                  - subscription.renewed
                  - invoice.paid
                  - invoice.payment_failed
                  - invoice.uncollectible
                  - invoice.updated
                fields:
                  customer:
                    - id
                    - email
                    - name
                    - country
                    - locale
                    - external_id
                  subscription:
                    - id
                    - status
                    - plan
                    - amount_cents
                    - currency
                    - interval
                    - interval_count
                    - current_period_end
                    - cancel_at_period_end
                    - started_at
                    - canceled_at
                    - ended_at
                  invoice:
                    - id
                    - subscription_id
                    - status
                    - amount_due_cents
                    - amount_paid_cents
                    - amount_remaining_cents
                    - currency
                    - attempt_count
                    - decline_code
                    - failure_code
                    - next_attempt_at
                    - hosted_invoice_url
                transforms:
                  - units_to_cents
                  - unix_seconds
                  - unix_millis
                  - lowercase
                  - boolean
                  - string
                sample:
                  - id: bws_UdGw7Rgf3vMuJkXV
                    received_at: 2026-11-04T10:00:00Z
                    payload:
                      id: wh_1
                      data:
                        email: ana@example.test
                        amount: "49.90"
                        invoice: inv_wh_1
                        currency: eur
                        customer: c_1
                      type: invoice.failed
                    result:
                      status: ignored
                      rule: null
                      type: null
                      event_id: null
                      data: null
                      errors: []
                summary:
                  total: 1
                  events: 0
                  errors: 0
                  ignored: 1
              schema:
                $ref: "#/components/schemas/BillingMapping"
      x-audience: public
    put:
      summary: Save the mapping rules (tested on the sample)
      tags:
        - Integrations
      operationId: updateBillingMapping
      parameters: []
      responses:
        "200":
          description: saved; each sample webhook with what it becomes
          content:
            application/json:
              example:
                object: billing_mapping
                billing_connection: bcn_mVxfYN2KgqTevEbE
                active: false
                activated_at: null
                rules:
                  - when:
                      path: $.type
                      equals: invoice.failed
                    type: invoice.payment_failed
                    event_id: $.id
                    fields:
                      customer.id: $.data.customer
                      customer.email: $.data.email
                      invoice.id: $.data.invoice
                      invoice.amount_due_cents:
                        path: $.data.amount
                        transform: units_to_cents
                      invoice.currency: $.data.currency
                types:
                  - customer.upserted
                  - subscription.created
                  - subscription.updated
                  - subscription.paused
                  - subscription.resumed
                  - subscription.canceled
                  - subscription.renewed
                  - invoice.paid
                  - invoice.payment_failed
                  - invoice.uncollectible
                  - invoice.updated
                fields:
                  customer:
                    - id
                    - email
                    - name
                    - country
                    - locale
                    - external_id
                  subscription:
                    - id
                    - status
                    - plan
                    - amount_cents
                    - currency
                    - interval
                    - interval_count
                    - current_period_end
                    - cancel_at_period_end
                    - started_at
                    - canceled_at
                    - ended_at
                  invoice:
                    - id
                    - subscription_id
                    - status
                    - amount_due_cents
                    - amount_paid_cents
                    - amount_remaining_cents
                    - currency
                    - attempt_count
                    - decline_code
                    - failure_code
                    - next_attempt_at
                    - hosted_invoice_url
                transforms:
                  - units_to_cents
                  - unix_seconds
                  - unix_millis
                  - lowercase
                  - boolean
                  - string
                sample:
                  - id: bws_rcEbEUxNJYg4jyS4
                    received_at: 2026-11-04T10:00:00Z
                    payload:
                      id: wh_1
                      data:
                        email: ana@example.test
                        amount: "49.90"
                        invoice: inv_wh_1
                        currency: eur
                        customer: c_1
                      type: invoice.failed
                    result:
                      status: event
                      rule: 0
                      type: invoice.payment_failed
                      event_id: wh_1
                      data:
                        invoice:
                          currency: eur
                          id: inv_wh_1
                          amount_due_cents: 4990
                        customer:
                          id: c_1
                          email: ana@example.test
                      errors: []
                summary:
                  total: 1
                  events: 1
                  errors: 0
                  ignored: 0
              schema:
                $ref: "#/components/schemas/BillingMapping"
        "400":
          description: invalid rules
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - rules
              properties:
                rules:
                  type: array
                  items:
                    type: object
                    additionalProperties: true
            example:
              rules:
                - when:
                    path: $.type
                    equals: invoice.failed
                  type: invoice.payment_failed
                  event_id: $.id
                  fields:
                    customer.id: $.data.customer
                    customer.email: $.data.email
                    invoice.id: $.data.invoice
                    invoice.amount_due_cents:
                      path: $.data.amount
                      transform: units_to_cents
                    invoice.currency: $.data.currency
        required: true
  /v1/billing_connections/{billing_connection_id}/mapping/activate:
    parameters:
      - name: billing_connection_id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Turn the mapping on (the sample must pass)
      tags:
        - Integrations
      operationId: activateBillingMapping
      responses:
        "200":
          description: on
          content:
            application/json:
              example:
                object: billing_mapping
                billing_connection: bcn_7ihcvkgpzms1J5ee
                active: true
                activated_at: 2026-11-04T10:00:00Z
                rules:
                  - type: invoice.payment_failed
                    when:
                      path: $.type
                      equals: invoice.failed
                    fields:
                      invoice.id: $.data.invoice
                      customer.id: $.data.customer
                      customer.email: $.data.email
                      invoice.currency: $.data.currency
                      invoice.amount_due_cents:
                        path: $.data.amount
                        transform: units_to_cents
                    event_id: $.id
                types:
                  - customer.upserted
                  - subscription.created
                  - subscription.updated
                  - subscription.paused
                  - subscription.resumed
                  - subscription.canceled
                  - subscription.renewed
                  - invoice.paid
                  - invoice.payment_failed
                  - invoice.uncollectible
                  - invoice.updated
                fields:
                  customer:
                    - id
                    - email
                    - name
                    - country
                    - locale
                    - external_id
                  subscription:
                    - id
                    - status
                    - plan
                    - amount_cents
                    - currency
                    - interval
                    - interval_count
                    - current_period_end
                    - cancel_at_period_end
                    - started_at
                    - canceled_at
                    - ended_at
                  invoice:
                    - id
                    - subscription_id
                    - status
                    - amount_due_cents
                    - amount_paid_cents
                    - amount_remaining_cents
                    - currency
                    - attempt_count
                    - decline_code
                    - failure_code
                    - next_attempt_at
                    - hosted_invoice_url
                transforms:
                  - units_to_cents
                  - unix_seconds
                  - unix_millis
                  - lowercase
                  - boolean
                  - string
                sample:
                  - id: bws_VCjNWusA1bhGbbGR
                    received_at: 2026-11-04T10:00:00Z
                    payload:
                      id: wh_1
                      data:
                        email: ana@example.test
                        amount: "49.90"
                        invoice: inv_wh_1
                        currency: eur
                        customer: c_1
                      type: invoice.failed
                    result:
                      status: event
                      rule: 0
                      type: invoice.payment_failed
                      event_id: wh_1
                      data:
                        invoice:
                          currency: eur
                          id: inv_wh_1
                          amount_due_cents: 4990
                        customer:
                          id: c_1
                          email: ana@example.test
                      errors: []
                summary:
                  total: 1
                  events: 1
                  errors: 0
                  ignored: 0
              schema:
                $ref: "#/components/schemas/BillingMapping"
        "409":
          description: the sample does not pass yet
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/billing_connections/{billing_connection_id}/mapping/deactivate:
    parameters:
      - name: billing_connection_id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Turn the mapping off (webhooks are only kept as a sample)
      tags:
        - Integrations
      operationId: deactivateBillingMapping
      responses:
        "200":
          description: off
          content:
            application/json:
              example:
                object: billing_mapping
                billing_connection: bcn_omV4hFm5EZYzNGTC
                active: false
                activated_at: null
                rules: []
                types:
                  - customer.upserted
                  - subscription.created
                  - subscription.updated
                  - subscription.paused
                  - subscription.resumed
                  - subscription.canceled
                  - subscription.renewed
                  - invoice.paid
                  - invoice.payment_failed
                  - invoice.uncollectible
                  - invoice.updated
                fields:
                  customer:
                    - id
                    - email
                    - name
                    - country
                    - locale
                    - external_id
                  subscription:
                    - id
                    - status
                    - plan
                    - amount_cents
                    - currency
                    - interval
                    - interval_count
                    - current_period_end
                    - cancel_at_period_end
                    - started_at
                    - canceled_at
                    - ended_at
                  invoice:
                    - id
                    - subscription_id
                    - status
                    - amount_due_cents
                    - amount_paid_cents
                    - amount_remaining_cents
                    - currency
                    - attempt_count
                    - decline_code
                    - failure_code
                    - next_attempt_at
                    - hosted_invoice_url
                transforms:
                  - units_to_cents
                  - unix_seconds
                  - unix_millis
                  - lowercase
                  - boolean
                  - string
                sample: []
                summary:
                  total: 0
                  events: 0
                  errors: 0
                  ignored: 0
              schema:
                $ref: "#/components/schemas/BillingMapping"
      x-audience: public
  /v1/invoices:
    get:
      summary: List invoices, optionally for one customer
      tags:
        - Invoices
      operationId: listInvoices
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
          description: cst_…, cus_… or your id.
        - name: stripe_customer
          in: query
          required: false
          schema:
            type: string
        - name: external_id
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: list
          content:
            application/json:
              example:
                object: list
                data:
                  - object: invoice
                    id: ninv_WL2dwPKmWFvGVwFH
                    customer: cst_LcPL9ouMxcj8urYd
                    subscription: nsub_GvBC2Nd8WchY2sb4
                    stripe_invoice_id: in_1
                    external_id: null
                    livemode: false
                    number: INV-1
                    status: open
                    amount_due_cents: 4900
                    amount_paid_cents: 0
                    amount_remaining_cents: 4900
                    currency: eur
                    attempt_count: 1
                    next_attempt_at: 2026-11-07T10:00:00.000Z
                    failure_code: null
                    decline_code: null
                    hosted_invoice_url: https://invoice.stripe.com/i/test
                    paid_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/InvoiceList"
        "404":
          description: unknown customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/payment_methods:
    get:
      summary: List payment methods, optionally for one customer
      tags:
        - Payment methods
      operationId: listPaymentMethods
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
          description: cst_…, cus_… or your id.
        - name: stripe_customer
          in: query
          required: false
          schema:
            type: string
        - name: external_id
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: list
          content:
            application/json:
              example:
                object: list
                data:
                  - object: payment_method
                    id: npm_ToLxz1qiAXPyXm2j
                    customer: cst_TDtZmEc1VKdDucfv
                    stripe_payment_method_id: pm_1
                    livemode: false
                    type: card
                    brand: visa
                    last4: "4242"
                    exp_month: 12
                    exp_year: 2030
                    funding: credit
                    country: FR
                    wallet: null
                    is_default: false
                    detached_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/PaymentMethodList"
        "404":
          description: unknown customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/flows/{id}/logos:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: cancel, or the cancel flow's flw_…
    post:
      summary: Upload a logo for the cancel modal (admin+)
      tags:
        - Flows
      operationId: uploadCancelFlowLogo
      description: "PNG, JPEG or WebP, 1 MB and 4000 px per side at most; the type is read from the file's bytes, not its name (SVG is refused: it can carry script). Stored by NoChurn (S3 in production). Returns the logo and its public url, served without auth at an unguessable path: put it in draft.design.logo_url (PATCH /v1/flows/cancel) and save the draft — the upload alone does not change the draft. Resource flows (write)."
      parameters: []
      responses:
        "201":
          description: stored, with cancel_flow.logo_uploaded
          content:
            application/json:
              example:
                object: cancel_flow_logo
                id: clg_a64GDD84khnhiTj2
                flow: flw_xYvYtMQxrYYHDSbn
                url: https://api.nochurn.sh/assets/logos/2yfX8g5SnJZbznKJCmpExdYFiSEyrQ6t.png
                content_type: image/png
                byte_size: 45
                width: 240
                height: 80
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CancelFlowLogo"
        "400":
          description: another flow than the cancel flow
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "413":
          description: over 1 MB
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "415":
          description: "not a PNG, JPEG or WebP (read from the bytes): an SVG named logo.png"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: PNG, JPEG or WebP, at most 1 MB.
            example:
              file: logo.png
        required: true
  /v1/flows/{id}/logos/{logo_id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: logo_id
        in: path
        required: true
        schema:
          type: string
        description: clg_…
    delete:
      summary: Remove a logo (admin+)
      tags:
        - Flows
      operationId: removeCancelFlowLogo
      description: Takes it out of the draft at once (design.logo_url becomes null when it was this logo). The file is deleted once no published version, segment or A/B variant uses it, so a running modal never loses its logo.
      responses:
        "200":
          description: removed, with cancel_flow.logo_removed; the flow with its draft
          content:
            application/json:
              example:
                object: flow
                id: flw_bv4fLvDSqHLBcmKP
                kind: cancel
                key: default
                name: Cancel flow
                status: draft
                holdout_percent: 5
                test_version: null
                live_version: null
                unpublished_changes:
                  - design
                mode_status:
                  livemode: false
                  state: draft
                  version: null
                  published_at: null
                  unpublished_changes:
                    - design
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  pause:
                    inapp: none
                  steps:
                    - reason
                    - offer
                    - feedback
                    - confirmation
                  texts: {}
                  design:
                    preset: light
                    logo_url: null
                    css_report:
                      errors: []
                      cancel_button_check: ok
                    custom_css: ""
                  reasons:
                    - key: too_expensive
                      labels:
                        de: Es ist zu teuer
                        en: It's too expensive
                        fr: C'est trop cher
                        it: È troppo caro
                        pt: É demasiado caro
                      offer_id: null
                    - key: missing_feature
                      labels:
                        de: Eine Funktion fehlt
                        en: A feature is missing
                        fr: Il manque une fonctionnalité
                        it: Manca una funzionalità
                        pt: Falta uma funcionalidade
                      offer_id: null
                    - key: not_using
                      labels:
                        de: Ich nutze es zu wenig
                        en: I don't use it enough
                        fr: Je ne l'utilise pas assez
                        it: Non lo uso abbastanza
                        pt: Não o uso o suficiente
                      offer_id: null
                    - key: switching
                      labels:
                        de: Ich wechsle zu einem anderen Tool
                        en: I'm switching to another tool
                        fr: Je passe à un autre outil
                        it: Passo a un altro strumento
                        pt: Vou mudar para outra ferramenta
                      offer_id: null
                    - key: temporary
                      labels:
                        de: Ich brauche nur eine Pause
                        en: I only need a break
                        fr: J'ai juste besoin d'une pause
                        it: Ho solo bisogno di una pausa
                        pt: Só preciso de uma pausa
                      offer_id: null
                    - key: project_finished
                      labels:
                        de: Mein Projekt ist abgeschlossen
                        en: My project is finished
                        fr: Mon projet est terminé
                        it: Il mio progetto è finito
                        pt: O meu projeto terminou
                      offer_id: null
                    - key: other
                      labels:
                        de: Etwas anderes
                        en: Something else
                        fr: Autre chose
                        it: Altro
                        pt: Outra coisa
                      offer_id: null
                      free_text: true
                  feedback:
                    enabled: true
                    required: false
                  cancel_mode: at_period_end
                  review_requests:
                    enabled: false
                    happy_reasons:
                      - project_finished
                      - too_expensive
                      - temporary
                    follow_up_email_days: 2
                  offers_once_per_customer_per_year: true
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
        "404":
          description: another account's logo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/cancel_flows/{id}:
    parameters:
      - name: id
        in: path
        description: default or flw_…
        required: true
        schema:
          type: string
      - name: locale
        in: query
        required: false
        schema:
          type: string
    get:
      summary: Cancel flow config for the widget or a native app
      tags:
        - Cancel flow
      operationId: getCancelFlow
      responses:
        "200":
          description: texts in the customer's language, reasons, theme
          content:
            application/json:
              example:
                object: cancel_flow
                id: flw_EmdD2FFD6aiqGNqz
                published: true
                version: 1
                locale: de
                texts:
                  review_ask: Würden Sie eine kurze Bewertung auf {{platform}} hinterlassen? Keine Belohnung, nur Ihre ehrliche Meinung.
                  review_cta: Bewertung schreiben
                  title: Bevor Sie gehen
                  reason_prompt: Warum kündigen Sie?
                  other_placeholder: Erzählen Sie uns mehr (optional)
                  offer_title: Eine letzte Sache
                  accept: Angebot annehmen
                  decline: Nein danke, weiter
                  feedback_prompt: Was hätten wir besser machen können?
                  feedback_submit: Weiter
                  confirm_title: Abonnement kündigen?
                  confirm_body: Sie behalten den Zugang bis zum Ende des bezahlten Zeitraums.
                  cancel_now: Mein Abonnement kündigen
                  saved_title: Danke, dass Sie bleiben!
                  cancelled_title: Ihr Abonnement ist gekündigt
                  cancelled_body: Sie behalten den Zugang bis zum Ende des bezahlten Zeitraums.
                  offer_discount: "{{percent}} % Rabatt für {{months}} Monate"
                  offer_pause: "{{months}} Monate kostenlos pausieren"
                  offer_downgrade: Zu {{plan}} wechseln
                  offer_talk: Mit dem Team sprechen
                  choose_subscription_title: Welches Abonnement?
                  choose_subscription_body: Sie haben mehrere Abonnements. Wählen Sie das, das Sie kündigen möchten.
                  choose_subscription_select: Mit diesem fortfahren
                  cancel_this_one: Dieses kündigen
                  renews_on: Verlängert sich am {{date}}
                reasons:
                  - key: too_expensive
                    label: Es ist zu teuer
                    free_text: false
                  - key: missing_feature
                    label: Eine Funktion fehlt
                    free_text: false
                  - key: not_using
                    label: Ich nutze es zu wenig
                    free_text: false
                  - key: switching
                    label: Ich wechsle zu einem anderen Tool
                    free_text: false
                  - key: temporary
                    label: Ich brauche nur eine Pause
                    free_text: false
                  - key: project_finished
                    label: Mein Projekt ist abgeschlossen
                    free_text: false
                  - key: other
                    label: Etwas anderes
                    free_text: true
                feedback_enabled: true
                feedback:
                  enabled: true
                  required: false
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                languages:
                  - en
                  - fr
                  - de
                  - it
                  - pt
                cancel_fallback_url: null
              schema:
                $ref: "#/components/schemas/CancelFlow"
      x-audience: browser
  /v1/cancel_sessions:
    post:
      summary: Open the cancel flow for a customer
      tags:
        - Cancel flow
      operationId: createCancelSession
      description: "From the browser: publishable key + X-NoChurn-Customer-Token + allowed Origin. From a server: secret key and customer. Click to cancel: POST /cancel works at every step; cancel_url_now is shown once. The subscription it targets: the token's `subscription` claim (browser) or `subscription` (server), else the customer's only live one; several and none named: the session starts at choose_subscription (DECISIONS 72)."
      parameters:
        - name: Origin
          in: header
          required: false
          schema:
            type: string
      responses:
        "201":
          description: session at the reason step
          content:
            application/json:
              example:
                object: cancel_session
                id: ses_CWSSquPBUBep38LF
                livemode: false
                step: reason
                outcome: null
                locale: en
                holdout: false
                published: true
                customer:
                  id: cst_8spCLa6W9qQzhEpt
                  stripe_customer_id: cus_1
                  external_id: user_42
                subscription:
                  id: nsub_dNjp6zubp5wmUDhA
                  plan: null
                  status: active
                  amount_cents: 4900
                  currency: eur
                  interval: month
                  interval_count: 1
                  current_period_end: 2026-11-24T10:00:00Z
                  cancel_at_period_end: false
                subscriptions: []
                reasons:
                  - key: too_expensive
                    label: It's too expensive
                    free_text: false
                  - key: missing_feature
                    label: A feature is missing
                    free_text: false
                  - key: not_using
                    label: I don't use it enough
                    free_text: false
                  - key: switching
                    label: I'm switching to another tool
                    free_text: false
                  - key: temporary
                    label: I only need a break
                    free_text: false
                  - key: project_finished
                    label: My project is finished
                    free_text: false
                  - key: other
                    label: Something else
                    free_text: true
                offer: null
                talk_url: null
                texts:
                  review_ask: Would you leave a short review on {{platform}}? No reward, just your honest opinion.
                  review_cta: Leave a review
                  title: Before you go
                  reason_prompt: Why are you cancelling?
                  other_placeholder: Tell us more (optional)
                  offer_title: One last thing
                  accept: Accept the offer
                  decline: No thanks, continue
                  feedback_prompt: Anything we could have done better?
                  feedback_submit: Continue
                  confirm_title: Cancel your subscription?
                  confirm_body: You keep access until the end of the paid period.
                  cancel_now: Cancel my subscription
                  saved_title: Thanks for staying!
                  cancelled_title: Your subscription is cancelled
                  cancelled_body: You keep access until the end of the paid period.
                  offer_discount: "{{percent}}% off for {{months}} months"
                  offer_pause: Pause for {{months}} months, free
                  offer_downgrade: Switch to {{plan}}
                  offer_talk: Talk to the team
                  choose_subscription_title: Which subscription?
                  choose_subscription_body: You have several subscriptions. Choose the one you want to cancel.
                  choose_subscription_select: Continue with this one
                  cancel_this_one: Cancel this one
                  renews_on: Renews on {{date}}
                feedback_enabled: true
                feedback:
                  enabled: true
                  required: false
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                cancel_url_now: http://localhost:3000/x/Il4clEDtUMUqj9it2-O37qsHnhoIWbq8
                cancel_status: null
                cancel_effective_at: null
                result: null
                access_until: null
                confirmation_email_sent: false
                review: null
                expires_at: 2026-11-05T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/CancelSession"
        "401":
          description: a publishable key without a customer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: an origin that is not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                customer:
                  type: string
                  example: user_42
                subscription:
                  type: string
                  example: sub_1PqX2
                  description: "Secret key only (from a browser: the token's `subscription` claim). nsub_…, sub_… or its id in your billing; a live subscription of that customer, else 404 subscription_not_found."
                locale:
                  type: string
                  enum:
                    - en
                    - fr
                    - de
                    - it
                    - pt
            example:
              customer: user_42
        required: true
    get:
      summary: Cancel sessions
      tags:
        - Cancel flow
      operationId: listCancelSessions
      parameters:
        - name: outcome
          in: query
          required: false
          schema:
            type: string
            enum:
              - saved
              - cancelled
              - abandoned
        - name: customer
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: newest first
          content:
            application/json:
              example:
                object: list
                data:
                  - object: cancel_session
                    id: ses_XsgUyosnL9TPcZiz
                    livemode: false
                    step: reason
                    outcome: null
                    locale: en
                    holdout: false
                    published: false
                    customer:
                      id: cst_H5m8w5V7ovfoLPQE
                      stripe_customer_id: null
                      external_id: null
                    subscription: null
                    subscriptions: []
                    reasons:
                      - key: too_expensive
                        label: It's too expensive
                        free_text: false
                      - key: missing_feature
                        label: A feature is missing
                        free_text: false
                      - key: not_using
                        label: I don't use it enough
                        free_text: false
                      - key: switching
                        label: I'm switching to another tool
                        free_text: false
                      - key: temporary
                        label: I only need a break
                        free_text: false
                      - key: project_finished
                        label: My project is finished
                        free_text: false
                      - key: other
                        label: Something else
                        free_text: true
                    offer: null
                    talk_url: null
                    texts:
                      review_ask: Would you leave a short review on {{platform}}? No reward, just your honest opinion.
                      review_cta: Leave a review
                      title: Before you go
                      reason_prompt: Why are you cancelling?
                      other_placeholder: Tell us more (optional)
                      offer_title: One last thing
                      accept: Accept the offer
                      decline: No thanks, continue
                      feedback_prompt: Anything we could have done better?
                      feedback_submit: Continue
                      confirm_title: Cancel your subscription?
                      confirm_body: You keep access until the end of the paid period.
                      cancel_now: Cancel my subscription
                      saved_title: Thanks for staying!
                      cancelled_title: Your subscription is cancelled
                      cancelled_body: You keep access until the end of the paid period.
                      offer_discount: "{{percent}}% off for {{months}} months"
                      offer_pause: Pause for {{months}} months, free
                      offer_downgrade: Switch to {{plan}}
                      offer_talk: Talk to the team
                      choose_subscription_title: Which subscription?
                      choose_subscription_body: You have several subscriptions. Choose the one you want to cancel.
                      choose_subscription_select: Continue with this one
                      cancel_this_one: Cancel this one
                      renews_on: Renews on {{date}}
                    feedback_enabled: true
                    feedback:
                      enabled: true
                      required: false
                    theme:
                      object: cancel_flow_theme
                      preset: light
                      format: modal
                      tokens:
                        bg: "#f5f6fa"
                        surface: "#ffffff"
                        text: "#1c1e4a"
                        muted: "#686b85"
                        accent: "#4637d9"
                        accent_text: "#ffffff"
                        border: "#e3e5ec"
                        danger: "#c2410c"
                        font: system-ui, sans-serif
                        font_size: 15
                        radius: 12
                        radius_control: 8
                        overlay: 0.5
                        max_width: 480
                        shadow: md
                        spacing: regular
                        border_width: 1
                        border_style: solid
                        font_scale: 1.25
                        font_weight_title: 600
                      font_url: null
                      brand: logo
                      logo_url: null
                      app_name: Account 1
                      end_screen:
                        style: celebrate
                        auto_close_seconds: 6
                      custom_css: ""
                      cancel_button_check: ok
                      classes:
                        - nc-modal
                        - nc-title
                        - nc-reason
                        - nc-offer
                        - nc-btn-primary
                        - nc-btn-cancel
                        - nc-footer
                    cancel_url_now: null
                    cancel_status: null
                    cancel_effective_at: null
                    result: null
                    access_until: null
                    confirmation_email_sent: false
                    review: null
                    expires_at: 2026-11-05T10:00:00Z
                    created_at: 2026-11-04T10:00:00Z
                    reason: too_expensive
                    comment: Pricey
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/CancelSessionReportList"
      x-audience: public
  /v1/offers:
    post:
      summary: Add an offer to the cancel flow's library
      tags:
        - Cancel flow
      operationId: createOffer
      parameters: []
      responses:
        "201":
          description: a discount that names a Maxio coupon
          content:
            application/json:
              example:
                object: offer
                id: off_9QeL5KwSdZsZnnwJ
                kind: discount
                name: 30% for 3 months
                params:
                  percent: 30
                  months: 3
                archived_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Offer"
        "422":
          description: a talk offer without an https link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - name
                - params
              properties:
                kind:
                  type: string
                  enum:
                    - discount
                    - pause
                    - downgrade
                    - talk
                name:
                  type: string
                params:
                  type: object
                  description: discount { percent, months, coupon_code? (Maxio coupon) } · pause { months } · downgrade { target_price_id, plan_name } · talk { url }
            example:
              kind: discount
              name: 30% for 3 months
              params:
                percent: 30
                months: 3
        required: true
    get:
      summary: Offer library
      tags:
        - Cancel flow
      operationId: listOffers
      parameters:
        - name: include_archived
          in: query
          required: false
          schema:
            type: boolean
      responses:
        "200":
          description: active offers
          content:
            application/json:
              example:
                object: list
                data:
                  - object: offer
                    id: off_KmCAXbU2dJBckeAo
                    kind: discount
                    name: 30% for 3 months
                    params:
                      months: 3
                      percent: 30
                    archived_at: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/OfferList"
      x-audience: public
  /v1/account/clients:
    get:
      summary: Versions of NoChurn's client code calling the API
      tags:
        - Account
      operationId: listClientVersions
      description: "Versions of NoChurn's own client code (the widget js.nochurn.sh, the MCP server, the SDKs) seen in the last 90 days in this mode, from the NoChurn-Client header (`<name>/<semver>`), per client with its latest and minimum supported version. Row status: current, update_available (below latest), unsupported (below minimum), unknown (a client NoChurn does not publish). The widget's origin_host is the host of the page that loaded it; api_key is the key used, oauth_client the AI app (MCP through OAuth). Rows are written at most once every 5 minutes: last_seen_at is within 5 minutes and calls_count is a lower bound. Resource account (read), every role. js.nochurn.sh/v1 updates itself within a major version: update_available for the widget means a pinned /v1.x.y.js URL, a self-hosted copy or an older major."
      responses:
        "200":
          description: the clients and their versions
          content:
            application/json:
              example:
                object: client_versions
                livemode: false
                window_days: 90
                data:
                  - object: client
                    client: widget
                    name: Cancel widget (js.nochurn.sh)
                    latest: 1.0.0
                    minimum: 1.0.0
                    status: unsupported
                    versions:
                      - id: clv_fv7a8FvU8CUJZJ8F
                        version: 1.0.0
                        status: current
                        origin_host: app.acme.com
                        api_key:
                          id: key_ytLQLbZVZ2GdVyUF
                          name: Key 1
                          kind: publishable
                          mode: test
                          last4: ntgP
                        oauth_client: null
                        first_seen_at: 2026-11-04T09:00:00.000Z
                        last_seen_at: 2026-11-04T09:00:00.000Z
                        calls_count: 3
                      - id: clv_nRfcm9YjNjXUcMqC
                        version: 0.9.0
                        status: unsupported
                        origin_host: app.acme.com
                        api_key:
                          id: key_ytLQLbZVZ2GdVyUF
                          name: Key 1
                          kind: publishable
                          mode: test
                          last4: ntgP
                        oauth_client: null
                        first_seen_at: 2026-11-02T10:00:00.000Z
                        last_seen_at: 2026-11-02T10:00:00.000Z
                        calls_count: 3
                  - object: client
                    client: mcp
                    name: NoChurn MCP server
                    latest: 0.1.0
                    minimum: 0.1.0
                    status: current
                    versions:
                      - id: clv_E1d1eodQeZi9yoUJ
                        version: 0.1.0
                        status: current
                        origin_host: null
                        api_key: null
                        oauth_client:
                          id: mcpc_EioDePP8sJxkMntr
                          name: Claude Code
                        first_seen_at: 2026-11-04T09:00:00.000Z
                        last_seen_at: 2026-11-04T09:00:00.000Z
                        calls_count: 3
                  - object: client
                    client: node
                    name: null
                    latest: null
                    minimum: null
                    status: unknown
                    versions:
                      - id: clv_L5QU6Xo2vi6b7TLV
                        version: 1.2.0
                        status: unknown
                        origin_host: null
                        api_key:
                          id: key_HCwE3oWH5fUAkZ91
                          name: Server
                          kind: secret
                          mode: test
                          last4: 51K6
                        oauth_client: null
                        first_seen_at: 2026-11-04T09:00:00.000Z
                        last_seen_at: 2026-11-04T09:00:00.000Z
                        calls_count: 3
              schema:
                $ref: "#/components/schemas/ClientVersions"
        "401":
          description: no credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customers/{id}/access:
    get:
      summary: May this customer use your app? (server-side wall)
      tags:
        - Customers
      operationId: getCustomerAccess
      description: "The truth behind NoChurn's in-app wall, for your backend: the widget's wall alone can be bypassed. Same calculation as GET /v1/inapp/state (wall.after_days, annual_grace_days; 0 = as soon as the payment fails). Secret key (customers: read). Webhooks customer.access_restricted / customer.access_restored tell you when it changes."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: cst_…, cus_… or your id.
        - name: stripe_account
          in: query
          required: false
          schema:
            type: string
          description: acct_… when the same cus_ exists on two connected Stripe accounts.
      responses:
        "200":
          description: "paid: full, nothing to pay"
          content:
            application/json:
              example:
                object: customer_access
                customer: cst_4w9Nj8EJEvWj7MDT
                livemode: false
                access: full
                reason: null
                since: null
                failing_since: 2026-11-04T10:00:00Z
                restricts_at: 2026-11-11T10:00:00Z
                invoice: ninv_1dMMAnaZyLmBNtDB
                pay_url: http://localhost:3000/c/Iyp0n60Dcd6R6xAK3UJ1MszddKUzHkeP
                subscription: null
                paused_at: null
                resumes_at: null
                cancel_url: http://localhost:3000/a/eyJfcmFpbHMiOnsiZGF0YSI6ImNzdF80dzlOajhFSkV2V2o3TURUIiwiZXhwIjoiMjAyNi0xMi0wNFQxMDowMDowMC4wMDBaIiwicHVyIjoiY3VzdG9tZXIvYWNjZXNzX2NhbmNlbCJ9fQ--46a89957572650047e29db93b65f0dc84d316720d722daa2cb2615d5fafc99f9
              schema:
                $ref: "#/components/schemas/CustomerAccess"
        "404":
          description: customer of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customers/summary:
    get:
      summary: The 4 figures of the Customers page
      tags:
        - Customers
      operationId: getCustomerSummary
      description: This month starts on the 1st in the account's time zone. Amounts are grouped by currency.
      responses:
        "200":
          description: summary
          content:
            application/json:
              example:
                object: customer_summary
                livemode: false
                timezone: Europe/Paris
                month_start: 2026-11-01T00:00:00+01:00
                active:
                  count: 1
                  new_this_month: 1
                high_risk:
                  threshold: 70
                  count: 1
                  mrr:
                    - currency: eur
                      cents: 4900
                  scored_customers: 1
                in_recovery:
                  count: 1
                  mrr:
                    - currency: eur
                      cents: 4900
                saved_this_month:
                  count: null
                  unavailable_reason: Counted from the recovery and cancel flows, not live yet.
              schema:
                $ref: "#/components/schemas/CustomerSummary"
      x-audience: public
  /v1/customers/{id}/profile:
    get:
      summary: Customer profile
      tags:
        - Customers
      operationId: getCustomerProfile
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: cst_…, cus_… or your id.
      responses:
        "200":
          description: profile
          content:
            application/json:
              example:
                object: customer_profile
                customer:
                  object: customer
                  id: cst_6KWhRc2vsEuErrbU
                  stripe_customer_id: cus_lena
                  external_id: u_lena
                  stripe_account: acct_A
                  livemode: false
                  email: customer1@acme-corp.dev
                  company_domain: acme-corp.dev
                  name: null
                  locale: null
                  country: null
                  region: null
                  timezone: Europe/Paris
                  timezone_source: account
                  attributes:
                    plan: pro
                    exports_30d: 46
                  churn_score: 82
                  churn_level: high
                  flag: null
                  state: in_recovery
                  plan: Pro
                  mrr_cents: 4900
                  lifetime_value_cents: 4900
                  currency: eur
                  payment_method:
                    id: npm_5g6sqZdgrBnJo1rw
                    type: card
                    brand: visa
                    last4: "4242"
                    exp_month: null
                    exp_year: null
                    funding: prepaid
                    wallet: apple_pay
                    risk_category: prepaid
                    label: Apple Pay · Visa •••• 4242
                  last_seen_at: null
                  churned_at: null
                  flows_paused_at: null
                  signup_at: null
                  created_at: 2026-11-04T10:00:00.000Z
                  updated_at: 2026-11-04T10:00:00.000Z
                  identifiers: []
                billing:
                  mrr_cents: 4900
                  currency: eur
                  lifetime_value_cents: 4900
                  lifetime_value_since: 2025-11-04T10:00:00.000Z
                  customer_since: 2026-11-04T10:00:00.000Z
                  payment_method:
                    id: npm_5g6sqZdgrBnJo1rw
                    type: card
                    brand: visa
                    last4: "4242"
                    exp_month: null
                    exp_year: null
                    funding: prepaid
                    wallet: apple_pay
                    risk_category: prepaid
                    label: Apple Pay · Visa •••• 4242
                  next_renewal:
                    at: 2026-11-14T10:00:00.000Z
                    amount_cents: 4900
                    currency: eur
                    subscription: nsub_9mqhquow11EnC1Px
                  subscriptions:
                    - id: nsub_9mqhquow11EnC1Px
                      plan: Pro
                      status: active
                      mrr_cents: 4900
                      currency: eur
                      interval: month
                      interval_count: 1
                      current_period_end: 2026-11-14T10:00:00.000Z
                      cancel_at_period_end: false
                score:
                  score: 82
                  level: high
                  factors: []
                  computed_at: null
                ai_analysis: null
                ai_analysis_status: missing
                attributes:
                  plan: pro
                  exports_30d: 46
                links:
                  stripe_dashboard: https://dashboard.stripe.com/acct_A/test/customers/cus_lena
              schema:
                $ref: "#/components/schemas/CustomerProfile"
        "404":
          description: customer of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customers/{id}/timeline:
    get:
      summary: Customer activity, newest first
      tags:
        - Customers
      operationId: getCustomerTimeline
      description: NoChurn events, payment attempts, subscription starts and cancellations.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: second page
          content:
            application/json:
              example:
                object: list
                data:
                  - object: timeline_item
                    id: nevt_TCnRkbneCoz1xsac
                    occurred_at: 2026-11-04T10:00:00.000Z
                    category: billing
                    type: payment_method.attached
                    summary: Card added
                    ref:
                      type: payment_method
                      id: npm_4VZffAUJcm7uFR8g
                    data: {}
                  - object: timeline_item
                    id: npay_EpsfXBzWuJC66pWy
                    occurred_at: 2026-11-03T10:00:00.000Z
                    category: billing
                    type: payment.failed
                    summary: €49.00 · failed · insufficient_funds
                    ref:
                      type: payment
                      id: npay_EpsfXBzWuJC66pWy
                    data:
                      amount_cents: 4900
                      amount_refunded_cents: 0
                      currency: eur
                      decline_code: insufficient_funds
                      risk_category: null
                      invoice: null
                  - object: timeline_item
                    id: nsub_wwcVS74Uv3QGxhqn.started
                    occurred_at: 2026-08-04T10:00:00.000Z
                    category: billing
                    type: subscription.started
                    summary: Pro · active
                    ref:
                      type: subscription
                      id: nsub_wwcVS74Uv3QGxhqn
                    data:
                      plan: Pro
                      status: active
                      mrr_cents: 4900
                      currency: eur
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/TimelinePage"
      x-audience: public
  /v1/customers/{id}/pause_flows:
    post:
      summary: Pause all flows for a customer
      tags:
        - Customers
      operationId: pauseCustomerFlows
      description: Stops the customer's active runs and blocks new ones until resume_flows.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: paused
          content:
            application/json:
              example:
                object: customer
                id: cst_8VVgYZN5XyepV47K
                stripe_customer_id: cus_lena
                external_id: u_lena
                stripe_account: acct_A
                livemode: false
                email: customer1@acme-corp.dev
                company_domain: acme-corp.dev
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes:
                  plan: pro
                  exports_30d: 46
                churn_score: 82
                churn_level: high
                flag: null
                state: in_recovery
                plan: Pro
                mrr_cents: 4900
                lifetime_value_cents: 4900
                currency: eur
                payment_method:
                  id: npm_2HFV26kN2xmX77qn
                  type: card
                  brand: visa
                  last4: "4242"
                  exp_month: null
                  exp_year: null
                  funding: prepaid
                  wallet: apple_pay
                  risk_category: prepaid
                  label: Apple Pay · Visa •••• 4242
                last_seen_at: null
                churned_at: null
                flows_paused_at: 2026-11-04T10:00:00.000Z
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers: []
              schema:
                $ref: "#/components/schemas/Customer"
      x-audience: public
  /v1/customers/{id}/resume_flows:
    post:
      summary: Resume flows for a customer
      tags:
        - Customers
      operationId: resumeCustomerFlows
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: resumed
          content:
            application/json:
              example:
                object: customer
                id: cst_yg7PWhJh6a5aucuT
                stripe_customer_id: cus_lena
                external_id: u_lena
                stripe_account: acct_A
                livemode: false
                email: customer1@acme-corp.dev
                company_domain: acme-corp.dev
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes:
                  plan: pro
                  exports_30d: 46
                churn_score: 82
                churn_level: high
                flag: null
                state: in_recovery
                plan: Pro
                mrr_cents: 4900
                lifetime_value_cents: 4900
                currency: eur
                payment_method:
                  id: npm_QWF1hjeTz5iFPdTE
                  type: card
                  brand: visa
                  last4: "4242"
                  exp_month: null
                  exp_year: null
                  funding: prepaid
                  wallet: apple_pay
                  risk_category: prepaid
                  label: Apple Pay · Visa •••• 4242
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers: []
              schema:
                $ref: "#/components/schemas/Customer"
      x-audience: public
  /v1/customer_duplicates/summary:
    get:
      summary: "Possible duplicates: count and scans"
      tags:
        - Customers
      operationId: getCustomerDuplicatesSummary
      description: Open proposals of this mode, the end of the last scan and the latest scan and merge all, with their real counts.
      responses:
        "200":
          description: summary of the current mode
          content:
            application/json:
              example:
                object: customer_duplicate_summary
                livemode: false
                open_count: 1
                last_scanned_at: 2026-11-04T10:00:00.000Z
                scan:
                  object: customer_duplicate_run
                  id: cdr_pvVTkKp4mB7Aoxqo
                  livemode: false
                  kind: scan
                  status: done
                  total: 2
                  processed: 2
                  found: 1
                  merged: 0
                  skipped: 0
                  failed: 0
                  failures: []
                  error: null
                  started_at: 2026-11-04T10:00:00.000Z
                  finished_at: 2026-11-04T10:00:00.000Z
                  created_at: 2026-11-04T10:00:00.000Z
                merge_all: null
              schema:
                $ref: "#/components/schemas/CustomerDuplicateSummary"
      x-audience: public
  /v1/customer_duplicates/scan:
    post:
      summary: Scan the customers for possible duplicates
      tags:
        - Customers
      operationId: scanCustomerDuplicates
      description: "Looks for possible duplicates among the customers of this mode already in base, with the same rules as customer matching (a shared current email; an id one customer holds that is another one's alias). In the background: poll GET /v1/customer_duplicates/summary. A pair already proposed, dismissed or merged is never proposed again. One at a time (a running scan is answered), once a minute at most (429). Owner or admin."
      responses:
        "202":
          description: scan queued
          content:
            application/json:
              example:
                object: customer_duplicate_run
                id: cdr_dSZC8EKunhNUWQrf
                livemode: false
                kind: scan
                status: queued
                total: 0
                processed: 0
                found: 0
                merged: 0
                skipped: 0
                failed: 0
                failures: []
                error: null
                started_at: null
                finished_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CustomerDuplicateRun"
        "429":
          description: scanned less than a minute ago
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customer_duplicates/merge_all:
    post:
      summary: Merge every open possible duplicate
      tags:
        - Customers
      operationId: mergeAllCustomerDuplicates
      description: "Irreversible. Merges each open proposal of this mode into its suggested_keep customer, one merge per pair (same as POST /v1/customer_duplicates/{id}/merge), oldest first: in a chain (A~B, B~C) the second merge targets the customer kept by the first. A pair that fails stays open with its reason in failures; the others go on. Up to 10 pairs: merged now (200); more: in the background (202, poll the summary). While one runs, it is answered. Owner or admin."
      responses:
        "200":
          description: merged now
          content:
            application/json:
              example:
                object: customer_duplicate_run
                id: cdr_CfHJreAxeF3b8VVy
                livemode: false
                kind: merge_all
                status: done
                total: 1
                processed: 1
                found: 0
                merged: 1
                skipped: 0
                failed: 0
                failures: []
                error: null
                started_at: 2026-11-04T10:00:00.000Z
                finished_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CustomerDuplicateRun"
        "202":
          description: "many pairs: in the background"
          content:
            application/json:
              example:
                object: customer_duplicate_run
                id: cdr_JM2D5yebmkpCcrDU
                livemode: false
                kind: merge_all
                status: queued
                total: 12
                processed: 0
                found: 0
                merged: 0
                skipped: 0
                failed: 0
                failures: []
                error: null
                started_at: null
                finished_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CustomerDuplicateRun"
      x-audience: public
  /v1/customers:
    get:
      summary: List customers
      tags:
        - Customers
      operationId: listCustomers
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Search email, name or any id, including every identifier a source gave (a tool's id, a previous email or id).
        - name: email
          in: query
          required: false
          schema:
            type: string
          description: Current or previous email.
        - name: level
          in: query
          required: false
          schema:
            type: string
          description: "Churn level, comma-separated: low, medium, high."
        - name: state
          in: query
          required: false
          schema:
            type: string
          description: "Comma-separated: none, trialing, active, in_recovery, paused, churned."
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum:
              - -created_at
              - -churn_score
              - -mrr
              - -last_seen_at
            default: -created_at
          description: Descending. Customers without a score (or last_seen_at) come last.
        - name: at_risk
          in: query
          required: false
          schema:
            type: boolean
          description: "true: a churn score of 70 or more, or an early signal (flag: card removed…), the rule of Home's at-risk renewals; false: the others."
        - name: renews_within_days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 90
          description: "A paying subscription (active or past_due) whose current period ends within this many days (set to cancel at the period end included: include[]=risk says so). With at_risk=true and 7: Home's at_risk_7d."
        - name: include[]
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              enum:
                - total_count
                - risk
          description: "total_count: count of the filtered list. risk: each customer's next renewal and score factors (risk)."
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: at risk and renewing within 7 days, with why and when
          content:
            application/json:
              example:
                object: list
                data:
                  - object: customer
                    id: cst_bTjTg5h1W9jXomCB
                    stripe_customer_id: cus_123
                    external_id: null
                    stripe_account: acct_A
                    livemode: false
                    email: ana@example.test
                    company_domain: null
                    name: null
                    locale: null
                    country: null
                    region: null
                    timezone: Europe/Paris
                    timezone_source: account
                    attributes: {}
                    churn_score: null
                    churn_level: null
                    flag: null
                    state: none
                    plan: null
                    mrr_cents: 0
                    lifetime_value_cents: 0
                    currency: null
                    payment_method: null
                    last_seen_at: null
                    churned_at: null
                    flows_paused_at: null
                    signup_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                    identifiers: []
                  - object: customer
                    id: cst_BMdHx29aVWcEdQLp
                    stripe_customer_id: null
                    external_id: u_123
                    stripe_account: null
                    livemode: false
                    email: bob@example.test
                    company_domain: null
                    name: null
                    locale: null
                    country: null
                    region: null
                    timezone: Europe/Paris
                    timezone_source: account
                    attributes: {}
                    churn_score: null
                    churn_level: null
                    flag: null
                    state: none
                    plan: null
                    mrr_cents: 0
                    lifetime_value_cents: 0
                    currency: null
                    payment_method: null
                    last_seen_at: null
                    churned_at: null
                    flows_paused_at: null
                    signup_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                    identifiers: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/CustomerPage"
        "400":
          description: renews_within_days out of range
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    post:
      summary: Create a customer (billing without Stripe, or before Stripe knows it)
      tags:
        - Customers
      operationId: createCustomer
      description: "Matched first (customer matching): when exactly one customer of this mode has this email and no other id of yours, it is linked to your external_id and returned with 200 instead of a duplicate. Several customers with the email: a new one is created and a possible duplicate To do is raised."
      parameters: []
      responses:
        "200":
          description: the only customer with this email, linked to your external_id
          content:
            application/json:
              example:
                object: customer
                id: cst_pzfcpuSfUFQSGkNf
                stripe_customer_id: cus_123
                external_id: u_ana
                stripe_account: acct_A
                livemode: false
                email: ana@example.test
                company_domain: null
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes: {}
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers:
                  - kind: email
                    provider: null
                    connection: null
                    value: ana@example.test
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
                  - kind: external
                    provider: null
                    connection: null
                    value: u_ana
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Customer"
        "201":
          description: created
          content:
            application/json:
              example:
                object: customer
                id: cst_ATdWVXLNrWdPxkSw
                stripe_customer_id: null
                external_id: u_456
                stripe_account: null
                livemode: false
                email: new@example.test
                company_domain: null
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes: {}
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers:
                  - kind: external
                    provider: null
                    connection: null
                    value: u_456
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
                  - kind: email
                    provider: null
                    connection: null
                    value: new@example.test
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Customer"
        "400":
          description: no identifier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: external_id already used
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                external_id:
                  type: string
                  description: "Recommended: the id in your app."
                email:
                  type: string
                name:
                  type: string
                locale:
                  type: string
                  enum:
                    - en
                    - de
                    - fr
                    - it
                    - pt
                country:
                  type: string
                region:
                  type: string
                  description: "State or province (US: TX)."
                timezone:
                  type: string
                  nullable: true
                  description: IANA zone. Wins over any guess; null goes back to the guess.
            example:
              external_id: u_456
              email: new@example.test
        required: true
  /v1/customers/resolve:
    get:
      summary: Resolve a customer from any of its ids
      tags:
        - Customers
      operationId: resolveCustomer
      description: Pass exactly one of customer (cst_…, cus_… or your id, resolved in that order), stripe_customer or external_id.
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: stripe_customer
          in: query
          required: false
          schema:
            type: string
        - name: external_id
          in: query
          required: false
          schema:
            type: string
        - name: stripe_account
          in: query
          required: false
          schema:
            type: string
          description: acct_… — needed when the same cus_ exists on two connected Stripe accounts.
      responses:
        "200":
          description: the customer and its three ids
          content:
            application/json:
              example:
                object: customer
                id: cst_ssJU64rijfqjh2XS
                stripe_customer_id: null
                external_id: u_123
                stripe_account: null
                livemode: false
                email: bob@example.test
                company_domain: null
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes: {}
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers: []
              schema:
                $ref: "#/components/schemas/Customer"
        "404":
          description: no match
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: ambiguous
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customers/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        description: cst_…, cus_… or your own id.
        required: true
      - name: stripe_account
        in: query
        required: false
        schema:
          type: string
    get:
      summary: Retrieve a customer
      tags:
        - Customers
      operationId: getCustomer
      responses:
        "200":
          description: customer
          content:
            application/json:
              example:
                object: customer
                id: cst_3WcGvYMRCF8rixYt
                stripe_customer_id: cus_123
                external_id: null
                stripe_account: acct_A
                livemode: false
                email: ana@example.test
                company_domain: null
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes: {}
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers: []
              schema:
                $ref: "#/components/schemas/Customer"
        "404":
          description: not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    patch:
      summary: Update a customer — e.g. link your own id to a Stripe customer
      tags:
        - Customers
      operationId: updateCustomer
      parameters: []
      responses:
        "200":
          description: time zone set by your app, then back to the guess
          content:
            application/json:
              example:
                object: customer
                id: cst_QwoaoX5sv4eucKBL
                stripe_customer_id: cus_123
                external_id: u_789
                stripe_account: acct_A
                livemode: false
                email: ana@example.test
                company_domain: null
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes: {}
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers:
                  - kind: email
                    provider: null
                    connection: null
                    value: ana@example.test
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
                  - kind: external
                    provider: null
                    connection: null
                    value: u_789
                    source: api
                    created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Customer"
        "409":
          description: external_id already used by another customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: unknown time zone
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                external_id:
                  type: string
                email:
                  type: string
                name:
                  type: string
                locale:
                  type: string
                  enum:
                    - en
                    - de
                    - fr
                    - it
                    - pt
                country:
                  type: string
                region:
                  type: string
                  description: "State or province (US: TX)."
                timezone:
                  type: string
                  nullable: true
                  description: IANA zone. Wins over any guess; null goes back to the guess.
            example:
              external_id: u_789
        required: true
  /v1/customer_duplicates:
    get:
      summary: List possible duplicate customers
      tags:
        - Customers
      operationId: listCustomerDuplicates
      description: "Two customers of this mode that may be the same person: they share an email, or one source named ids held by both. NoChurn never merges customers automatically; each proposal is also a To do (possible_duplicate), which a human dismisses or merges. suggested_keep: the customer to preselect (a live subscription, else billed by a tool, else the oldest)."
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - dismissed
              - merged
            default: open
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: open proposals of the current mode
          content:
            application/json:
              example:
                object: list
                data:
                  - object: customer_duplicate
                    id: cdp_Mu1X3FMJ79pDfrKj
                    livemode: false
                    customer: cst_jhuithY715hV64ga
                    other_customer: cst_y4w5qHA2hRDq9mFK
                    people:
                      - id: cst_jhuithY715hV64ga
                        name: null
                        email: ana@example.test
                      - id: cst_y4w5qHA2hRDq9mFK
                        name: null
                        email: ana@example.test
                    identifier: email
                    reason: share an email
                    status: open
                    suggested_keep: cst_y4w5qHA2hRDq9mFK
                    kept_customer: null
                    dismissed_at: null
                    merged_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/CustomerDuplicateList"
        "400":
          description: unknown status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customer_duplicates/{id}/dismiss:
    post:
      summary: Dismiss a possible duplicate (not the same person)
      tags:
        - Customers
      operationId: dismissCustomerDuplicate
      description: Both customers stay as they are; the pair is never proposed again.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: dismissed
          content:
            application/json:
              example:
                object: customer_duplicate
                id: cdp_mmgZiHxMMvfqnn8G
                livemode: false
                customer: cst_1Xto5g95GnymjNZJ
                other_customer: cst_gD495DbT1fr7KmA1
                people:
                  - id: cst_1Xto5g95GnymjNZJ
                    name: null
                    email: ana@example.test
                  - id: cst_gD495DbT1fr7KmA1
                    name: null
                    email: bob@example.test
                identifier: email
                reason: share an email
                status: dismissed
                suggested_keep: null
                kept_customer: null
                dismissed_at: 2026-11-04T10:00:00.000Z
                merged_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CustomerDuplicate"
        "404":
          description: unknown or in another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customer_duplicates/{id}/merge:
    post:
      summary: Merge a possible duplicate (the same person)
      tags:
        - Customers
      operationId: mergeCustomerDuplicate
      description: "Irreversible. Everything the other customer has (subscriptions, invoices, payments, cards, flow runs, messages, cancel sessions, disputes, timeline, attributes, usage, identifiers) moves to `keep`, then the other customer is deleted. Its ids stay known: its cst_, Stripe and tool ids, external_id and email are identifiers of the kept customer, so webhooks and API calls that still name it reach the kept one. The kept customer's own values win (the other external_id becomes a previous id). Owner or admin. The same merge again answers the same proposal; another choice once merged, or a dismissed proposal, is a 409."
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: "the same merge again: the same answer"
          content:
            application/json:
              example:
                object: customer_duplicate
                id: cdp_GtWYvMRMiVrhHU3a
                livemode: false
                customer: cst_2qdTqvEmV9hnpr9z
                other_customer: cst_inVEBWu2hf88HdGP
                people:
                  - null
                  - id: cst_inVEBWu2hf88HdGP
                    name: null
                    email: ana@example.test
                identifier: email
                reason: share an email
                status: merged
                suggested_keep: cst_inVEBWu2hf88HdGP
                kept_customer: cst_inVEBWu2hf88HdGP
                dismissed_at: null
                merged_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/CustomerDuplicate"
        "400":
          description: keep is not one of the two
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: in another account or the other mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: already merged into the other customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - keep
              properties:
                keep:
                  type: string
                  description: "The customer to keep: one of the two (suggested_keep is the default to offer)."
            example:
              keep: cst_inVEBWu2hf88HdGP
        required: true
  /v1/offers/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Edit an offer
      tags:
        - Cancel flow
      operationId: updateOffer
      description: Published flow versions keep their frozen copy of the offer.
      parameters: []
      responses:
        "200":
          description: edited
          content:
            application/json:
              example:
                object: offer
                id: off_vQW5kVg4HApSZfQX
                kind: discount
                name: 25% for 3 months
                params:
                  percent: 25
                  months: 3
                archived_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Offer"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                params:
                  type: object
            example:
              name: 25% for 3 months
              params:
                percent: 25
                months: 3
        required: true
    delete:
      summary: Archive an offer
      tags:
        - Cancel flow
      operationId: archiveOffer
      responses:
        "200":
          description: archived (published flows keep their copy)
          content:
            application/json:
              example:
                object: offer
                id: off_ZUAX1WJmcBxXHowk
                kind: discount
                name: 30% for 3 months
                params:
                  months: 3
                  percent: 30
                archived_at: 2026-11-04T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Offer"
      x-audience: public
  /v1/cancel_sessions/{id}/replay:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    get:
      summary: Replay a cancel session
      tags:
        - Cancel flow
      operationId: replayCancelSession
      responses:
        "200":
          description: the timestamped steps
          content:
            application/json:
              example:
                object: cancel_session_replay
                id: ses_AMW6ttCwGtJUgAYx
                steps:
                  - at: 2026-11-04T10:00:00Z
                    step: reason
                reason: null
                comment: null
                feedback: null
                outcome: null
                cancel_status: null
              schema:
                $ref: "#/components/schemas/CancelSessionReplay"
      x-audience: public
  /v1/review_platforms/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Edit or disable a review page
      tags:
        - Cancel flow
      operationId: updateReviewPlatform
      parameters: []
      responses:
        "200":
          description: edited
          content:
            application/json:
              example:
                object: review_platform
                id: rvp_WdXYG22G1uHGFB2V
                kind: g2
                url: https://g2.com/acme
                status: disabled
                review_count: 42
                rating: null
                rules: {}
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/ReviewPlatform"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                review_count:
                  type: integer
                rating:
                  type: number
                status:
                  type: string
                  enum:
                    - active
                    - disabled
                rules:
                  type: object
            example:
              review_count: 42
              status: disabled
        required: true
    delete:
      summary: Remove a review page
      tags:
        - Cancel flow
      operationId: deleteReviewPlatform
      description: Deleted, or disabled when reviews were already requested on it.
      responses:
        "204":
          description: removed
      x-audience: public
  /v1/replies/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    get:
      summary: One reply
      tags:
        - Messages
      operationId: getReply
      responses:
        "200":
          description: the reply
          content:
            application/json:
              example:
                object: message_reply
                id: rpl_QvzvaP4p8DDp6VTq
                message: msg_EBNrBPU9L5SuTsjj
                customer: cst_mz3wQqTbGGtZ36N6
                customer_email: ana@example.test
                customer_name: null
                flow_run: null
                flow: null
                from: ana@example.test
                subject: "Re: Hi"
                text: Thanks?
                category: question
                summary: null
                needs_human: true
                status: open
                handled_at: null
                answer_message: null
                received_at: 2026-11-04T10:00:00Z
                livemode: false
              schema:
                $ref: "#/components/schemas/MessageReply"
        "404":
          description: unknown
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/flows/{id}/stats:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        description: flw_… or the kind (prevention, recovery, cancel, winback).
        required: true
      - name: period
        in: query
        required: false
        schema:
          type: string
          enum:
            - month
            - 30d
            - 90d
            - 12m
          default: month
    get:
      summary: KPIs, funnel and holdout of a flow
      tags:
        - Flows
      operationId: getFlowStats
      responses:
        "200":
          description: the recovery flow this month
          content:
            application/json:
              example:
                object: flow_stats
                flow: flw_JgjjWhZqyvzdjbiw
                kind: recovery
                livemode: false
                period:
                  key: month
                  from: 2026-11-01T00:00:00+01:00
                  to: 2026-11-04T11:00:00+01:00
                kpis:
                  - key: revenue_recovered
                    label: Revenue recovered
                    value: 4900
                    unit: money
                    sample_size: 1
                    pending: 0
                    currency: eur
                    breakdown:
                      - currency: eur
                        amount_cents: 4900
                        converted_cents: 4900
                        unconverted_cents: 0
                  - key: recovery_rate
                    label: Recovery rate
                    value: 1
                    unit: rate
                    sample_size: 1
                  - key: in_progress
                    label: In progress
                    value: 1
                    unit: count
                    sample_size: 1
                  - key: median_days_to_recover
                    label: Median time to recover
                    value: 0
                    unit: days
                    sample_size: 1
                funnel:
                  - key: payment_failed
                    label: Payment failed
                    count: 2
                    share: 1
                  - key: smart_retry
                    label: Smart retry
                    count: 1
                    share: 0.5
                  - key: ai_email
                    label: AI email
                    count: 0
                    share: 0
                  - key: card_page
                    label: Card update page
                    count: 0
                    share: 0
                  - key: recovered
                    label: Recovered
                    count: 1
                    share: 0.5
                holdout:
                  percent: 0
                  runs: 0
                  natural_rate: null
                  treated_rate: 0
                  lift: null
                  sample_size: 2
              schema:
                $ref: "#/components/schemas/FlowStats"
        "400":
          description: unknown period
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/usage_events:
    post:
      summary: Track a usage event
      tags:
        - Data & AI
      operationId: createUsageEvent
      description: Rolled up each night into last_active_at, events_30d and <key event>_30d (custom attributes). Deduplicated on idempotency_key. Never sent to the AI.
      parameters: []
      responses:
        "202":
          description: accepted
          content:
            application/json:
              example:
                object: usage_event_batch
                accepted: 1
                inserted: 1
                duplicates: 0
                errors: []
              schema:
                $ref: "#/components/schemas/UsageEventBatch"
        "400":
          description: unknown event name format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: unknown customer (its own code and fix)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - customer
                - name
              properties:
                customer:
                  type: string
                  description: cst_…, cus_… or your own id.
                name:
                  type: string
                  example: export_used
                timestamp:
                  type: string
                  format: date-time
                properties:
                  type: object
                  additionalProperties: true
                  description: Any JSON object (50 keys kept).
                idempotency_key:
                  type: string
            example:
              customer: u_1
              name: export_used
              idempotency_key: e1
        required: true
  /v1/usage_events/batch:
    post:
      summary: Track up to 1,000 events
      tags:
        - Data & AI
      operationId: createUsageEventBatch
      parameters: []
      responses:
        "202":
          description: errors are reported per event
          content:
            application/json:
              example:
                object: usage_event_batch
                accepted: 1
                inserted: 1
                duplicates: 0
                errors:
                  - index: 1
                    code: customer_not_found
                    message: No customer matches "u_404" in test mode.
              schema:
                $ref: "#/components/schemas/UsageEventBatch"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - events
              properties:
                events:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required:
                      - customer
                      - name
                    properties:
                      customer:
                        type: string
                        description: cst_…, cus_… or your own id.
                      name:
                        type: string
                      timestamp:
                        type: string
                        format: date-time
                      properties:
                        type: object
                        additionalProperties: true
                        description: Any JSON object (50 keys kept).
                      idempotency_key:
                        type: string
            example:
              events:
                - customer: u_1
                  name: login
                - customer: u_404
                  name: login
        required: true
  /v1/insights:
    get:
      summary: Churn drivers and AI insights
      tags:
        - Data & AI
      operationId: getInsights
      description: The last statistics run (relative risk, Wilson interval, Benjamini-Hochberg q-value per bucket; sample_ok false under 30 customers or 5 departures) and the last validated AI insight.
      responses:
        "200":
          description: nothing computed yet
          content:
            application/json:
              example:
                object: insights
                run: null
                insight: null
              schema:
                $ref: "#/components/schemas/Insights"
      x-audience: public
  /v1/insights/refresh:
    post:
      summary: Recompute now (once an hour)
      tags:
        - Data & AI
      operationId: refreshInsights
      responses:
        "202":
          description: queued
          content:
            application/json:
              example:
                object: insights_refresh
                queued: true
              schema:
                $ref: "#/components/schemas/InsightsRefresh"
        "429":
          description: already refreshed this hour
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/campaigns:
    parameters:
      - name: group_by
        in: query
        required: false
        schema:
          type: string
          enum:
            - utm_campaign
            - utm_source
            - utm_medium
          default: utm_campaign
      - name: period
        in: query
        required: false
        schema:
          type: string
          enum:
            - 90d
            - 12m
          default: 90d
    get:
      summary: Churn by acquisition campaign
      tags:
        - Data & AI
      operationId: getCampaigns
      description: From utm_* attributes of customers who signed up in the period. Correlation, not causation.
      responses:
        "200":
          description: rows with their sample size
          content:
            application/json:
              example:
                object: campaign_stats
                group_by: utm_campaign
                period: 90d
                customers: 1
                churned: 0
                churn_rate: 0
                tracked: 1
                best: null
                worst: null
                min_sample: 30
                rows:
                  - value: black_friday
                    customers: 1
                    churned: 0
                    churn_rate: 0
                    relative_risk: null
                    avg_ltv_cents: 0
                    avg_score: null
                    mrr_lost_cents: 0
                    sample_ok: false
              schema:
                $ref: "#/components/schemas/CampaignStats"
      x-audience: public
  /v1/disputes:
    get:
      summary: Disputes
      tags:
        - Chargebacks
      operationId: listDisputes
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Comma separated statuses.
        - name: provider
          in: query
          required: false
          schema:
            type: string
        - name: due_before
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum:
              - opened_at
              - evidence_due_by
            default: opened_at
          description: "opened_at: newest first. evidence_due_by: soonest deadline first, disputes without a deadline last."
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: newest first, without card data
          content:
            application/json:
              example:
                object: list
                data:
                  - id: dsp_2F9vssKp8BhfWwe9
                    object: dispute
                    livemode: false
                    provider: stripe
                    billing_connection: scn_VDpCSHRTXyhtofFq
                    external_id: dp_1
                    charge: ch_1
                    customer: cst_5B4srTLYwe9m6DyR
                    customer_name: Lena
                    customer_email: customer1@acme-corp.dev
                    customer_plan: Pro
                    amount_cents: 4900
                    currency: eur
                    reason: subscription_canceled
                    status: needs_response
                    evidence_due_by: 2026-11-09T10:00:00Z
                    opened_at: 2026-11-04T10:00:00Z
                    outcome_at: null
                    submitted_at: null
                    submitted_by: null
                    provider_submitted: false
                    read_only: false
                    capability: evidence_api
                    evidence_count: 2
                    draft_status: draft
                    win_score: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/DisputeList"
      x-audience: public
  /v1/disputes/summary:
    get:
      summary: Counts of the Disputes tab (each with its sample size)
      tags:
        - Chargebacks
      operationId: disputesSummary
      parameters:
        - name: days
          in: query
          required: false
          schema:
            type: integer
            default: 90
      responses:
        "200":
          description: the saved amount counts only disputes won
          content:
            application/json:
              example:
                object: disputes_summary
                livemode: false
                days: 90
                open: 1
                due_within_72h: 0
                next_due_at: 2026-11-09T10:00:00Z
                drafts_ready: 1
                open_amount:
                  - currency: eur
                    amount_cents: 4900
                won: 0
                lost: 0
                decided: 0
                win_rate: null
                win_score_accuracy:
                  decided: 0
                  right: 0
                  rate: null
                won_amount: []
                fraud_warnings_open: 0
                fraud_warnings_refunded: 0
              schema:
                $ref: "#/components/schemas/DisputesSummary"
      x-audience: public
  /v1/disputes/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    get:
      summary: A dispute with its evidence and draft
      tags:
        - Chargebacks
      operationId: getDispute
      responses:
        "200":
          description: found
          content:
            application/json:
              example:
                id: dsp_nu5bHBnJb3n4KEEQ
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_37LhkkzpDAwwa5cU
                external_id: dp_1
                charge: ch_1
                customer: cst_JzQ8sUdXtXqzqq7x
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 2
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_iqCLHm6bhLAX1ZDa
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_kgW2ZRLXZHVnuFmk
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_GoqrRZdQYbEjCerZ
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_iqCLHm6bhLAX1ZDa
                    - dev_kgW2ZRLXZHVnuFmk
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: null
                  reason: No evidence from a record. Set the cost of contesting in Chargebacks › Settings to compare it with the amount.
                  evidence_count: 0
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "404":
          description: another account's dispute
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/disputes/{id}/evidence:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Include, exclude, reorder or add evidence
      tags:
        - Chargebacks
      operationId: updateDisputeEvidence
      parameters: []
      responses:
        "200":
          description: edited
          content:
            application/json:
              example:
                id: dsp_Rj5wjxQEaNy9JPW1
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_QKuRXU8hJQqAHwRc
                external_id: dp_1
                charge: ch_1
                customer: cst_iHbcRo3tbNDxfwMv
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 3
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_sfrkQKYjndBpn7sB
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: false
                    position: 1
                    file: null
                  - id: dev_DV9iYbojhjifRhC7
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                  - id: dev_qdxtS4tUW5xDEPZ4
                    object: dispute_evidence_item
                    kind: other
                    title: Terms accepted at signup
                    content: v3, 2026-01-04
                    source_type: manual
                    source_id: 54c0dcfe-ff8a-42ce-a4bb-720a027198e9
                    occurred_at: null
                    included: true
                    position: 3
                    file: null
                draft:
                  id: ddr_E1iWTCka5FaM988J
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_sfrkQKYjndBpn7sB
                    - dev_DV9iYbojhjifRhC7
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: contest
                  reason: 1 piece of evidence from NoChurn's records.
                  evidence_count: 1
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  items:
                    type: object
                    required:
                      - id
                    properties:
                      id:
                        type: string
                      included:
                        type: boolean
                      position:
                        type: integer
                add:
                  type: object
                  required:
                    - title
                  properties:
                    title:
                      type: string
                    content:
                      type: string
            example:
              items:
                - id: dev_sfrkQKYjndBpn7sB
                  included: false
              add:
                title: Terms accepted at signup
                content: v3, 2026-01-04
  /v1/disputes/{id}/evidence_files:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Attach a file as evidence (admin+)
      tags:
        - Chargebacks
      operationId: addDisputeEvidenceFile
      description: PDF, JPEG or PNG, 5 MB at most per file; the type is read from the file's bytes, not its name. For a Stripe dispute, one file per evidence field (the field of its kind, then uncategorized_file) and 4.5 MB of included files in all. Kept by NoChurn until the reply is sent, then given to the provider (Stripe Files API, Razorpay documents); a provider that takes no files (Braintree) gets the file named in the reply. Refused once the reply is sent or for a dispute the provider handles.
      parameters: []
      responses:
        "201":
          description: attached, with dispute.evidence_file_added
          content:
            application/json:
              example:
                id: dsp_Na6rvG7ku8MtRaDv
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_mLiVFHx89vq9XnXZ
                external_id: dp_1
                charge: ch_1
                customer: cst_64GTYRa6AoYRm1H7
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 3
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_RfiKG9mqpf14mwbe
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_AkhDCxNfD6Kt6Gd9
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                  - id: dev_jTobuAp3akY663Wa
                    object: dispute_evidence_item
                    kind: policy
                    title: Terms accepted at signup
                    content: "File attached by the merchant: terms.pdf (PDF, 51 Bytes)."
                    source_type: file
                    source_id: 9df4060a-9560-4835-9636-af43eaf834e7
                    occurred_at: 2026-11-04T10:00:00Z
                    included: true
                    position: 3
                    file:
                      filename: terms.pdf
                      content_type: application/pdf
                      byte_size: 51
                      sent_to_provider: false
                draft:
                  id: ddr_VxjQntJkeR1w9xhb
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_RfiKG9mqpf14mwbe
                    - dev_AkhDCxNfD6Kt6Gd9
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: contest
                  reason: 1 piece of evidence from NoChurn's records.
                  evidence_count: 1
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "409":
          description: the provider handles this dispute
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "413":
          description: over 5 MB
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "415":
          description: not a PDF, JPEG or PNG (read from the bytes)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF, JPEG or PNG, at most 5 MB.
                kind:
                  type: string
                  enum:
                    - communication
                    - policy
                    - usage
                    - reminder
                    - cancel_history
                    - refund_history
                    - receipt
                    - other
                  default: other
                  description: What the file proves; picks its Stripe evidence field.
                title:
                  type: string
                  maxLength: 200
                  description: Defaults to the file name.
            example:
              file: terms.pdf
              kind: policy
              title: Terms accepted at signup
        required: true
  /v1/disputes/{id}/evidence_files/{file_id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
      - name: file_id
        in: path
        schema:
          type: string
        description: The evidence item (dev_…) that holds the file.
        required: true
    delete:
      summary: Remove an attached file before sending (admin+)
      tags:
        - Chargebacks
      operationId: removeDisputeEvidenceFile
      responses:
        "200":
          description: removed, with dispute.evidence_file_removed
          content:
            application/json:
              example:
                id: dsp_YLweuRk1thhtFAYb
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_mCEQdXjjESrDrJDG
                external_id: dp_1
                charge: ch_1
                customer: cst_FtWgoBbNnircx8KD
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 2
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_4wYdXTX8SmoX9sxY
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_e4s6jyuCNDEvXSnz
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_hrCWnUdvgHduoCqh
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_4wYdXTX8SmoX9sxY
                    - dev_e4s6jyuCNDEvXSnz
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: null
                  reason: No evidence from a record. Set the cost of contesting in Chargebacks › Settings to compare it with the amount.
                  evidence_count: 0
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "404":
          description: gathered evidence (exclude it instead) or another dispute's file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: the reply was already sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/disputes/{id}/draft:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Draft the reply again (AI, from the included evidence)
      tags:
        - Chargebacks
      operationId: regenerateDisputeDraft
      responses:
        "201":
          description: drafted
          content:
            application/json:
              example:
                id: dsp_HtxpkYjWAKTfm2Up
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_J9fwTTz5CCnMQ9aH
                external_id: dp_1
                charge: ch_1
                customer: cst_tTkMw8fApKsvgKJS
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 2
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_btvZe1j2d5g3NpzY
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_QTqAuzsLxD1tJMvd
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_TZhtPxA84uU2PQ3i
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_btvZe1j2d5g3NpzY
                    - dev_QTqAuzsLxD1tJMvd
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: null
                  reason: No evidence from a record. Set the cost of contesting in Chargebacks › Settings to compare it with the amount.
                  evidence_count: 0
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
      x-audience: public
    patch:
      summary: Edit or approve the reply
      tags:
        - Chargebacks
      operationId: updateDisputeDraft
      parameters: []
      responses:
        "200":
          description: edited, with dispute.draft_edited
          content:
            application/json:
              example:
                id: dsp_qsKJ2P9jLqWDH6Ef
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_ZZJu5H56QFUahsJ1
                external_id: dp_1
                charge: ch_1
                customer: cst_ugS68ZNSH6gn8HwH
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 2
                draft_status: approved
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_SUnk9iZDSZbDHAQh
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_tiRpmG1reTLXsQag
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_iN69iJm2qEJVTAcB
                  object: dispute_draft
                  text: "We contest: the customer never asked to cancel."
                  cited_item_ids:
                    - dev_SUnk9iZDSZbDHAQh
                    - dev_tiRpmG1reTLXsQag
                  model: template
                  status: approved
                  edited_by: key_V7qug9n5dKhA1X9u
                  edited_at: 2026-11-04T10:00:00Z
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: null
                  reason: No evidence from a record. Set the cost of contesting in Chargebacks › Settings to compare it with the amount.
                  evidence_count: 0
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: null
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
                status:
                  type: string
                  enum:
                    - draft
                    - approved
            example:
              text: "We contest: the customer never asked to cancel."
              status: approved
  /v1/disputes/{id}/submit:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Send the reply to the provider (admin+, final)
      tags:
        - Chargebacks
      operationId: submitDispute
      parameters: []
      responses:
        "200":
          description: sent once
          content:
            application/json:
              example:
                id: dsp_QzSx4QbyyTkisnRu
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_DZQ97JHJBDsX7jrm
                external_id: dp_1
                charge: ch_1
                customer: cst_LLSMeaxBDkN75sB1
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: under_review
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: 2026-11-04T10:00:00Z
                submitted_by: key_Gb3rYm1g8cgZ6EtX
                provider_submitted: false
                read_only: true
                capability: evidence_api
                evidence_count: 2
                draft_status: sent
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_886VuUUzD8FfhMYo
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_vo1o3ad11iPAmsYJ
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_Ryx7fL7364Rn2p6h
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_886VuUUzD8FfhMYo
                    - dev_vo1o3ad11iPAmsYJ
                  model: template
                  status: sent
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "400":
          description: no explicit confirmation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: already submitted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - confirm
              properties:
                confirm:
                  type: boolean
                  enum:
                    - true
                text:
                  type: string
            example:
              confirm: true
  /v1/disputes/{id}/accept:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Accept the dispute (admin+, final)
      tags:
        - Chargebacks
      operationId: acceptDispute
      parameters: []
      responses:
        "200":
          description: accepted
          content:
            application/json:
              example:
                id: dsp_8BSADXr3AJYpLChN
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_6SJXLf7oAZC72G86
                external_id: dp_1
                charge: ch_1
                customer: cst_AQJjCENwQCV4zoni
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: accepted
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: 2026-11-04T10:00:00Z
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: true
                capability: evidence_api
                evidence_count: 2
                draft_status: draft
                win_score: null
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_zeBXp3xgZYonNaAr
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_cCLChQGq6RWCBhqN
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_qEE9rTYVtmPtMSid
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_zeBXp3xgZYonNaAr
                    - dev_cCLChQGq6RWCBhqN
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "409":
          description: the provider handles this dispute (merchant of record)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - confirm
              properties:
                confirm:
                  type: boolean
                  enum:
                    - true
            example:
              confirm: true
  /v1/disputes/{id}/win_score:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Set the chance of winning yourself, or give it back to the AI (admin+)
      tags:
        - Chargebacks
      operationId: setDisputeWinScore
      parameters: []
      responses:
        "200":
          description: set, with dispute.win_score_set
          content:
            application/json:
              example:
                id: dsp_LAPfbdgzPUevBEks
                object: dispute
                livemode: false
                provider: stripe
                billing_connection: scn_ZhJbRPTgnePzxjVs
                external_id: dp_1
                charge: ch_1
                customer: cst_jG4aKZ2xDye6LEtD
                customer_name: Lena
                customer_email: customer1@acme-corp.dev
                customer_plan: Pro
                amount_cents: 4900
                currency: eur
                reason: subscription_canceled
                status: needs_response
                evidence_due_by: 2026-11-09T10:00:00Z
                opened_at: 2026-11-04T10:00:00Z
                outcome_at: null
                submitted_at: null
                submitted_by: null
                provider_submitted: false
                read_only: false
                capability: evidence_api
                evidence_count: 2
                draft_status: draft
                win_score:
                  score: 65
                  source: manual
                  confidence: null
                  factors: []
                  evidence_count: null
                  model: null
                  set_by: key_KtYf839pxVaXiWsn
                  computed_at: 2026-11-04T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
                evidence:
                  - id: dev_JdC44XCLiiz2kLxu
                    object: dispute_evidence_item
                    kind: cancel_history
                    title: No cancellation request
                    content: The customer never opened the cancellation flow before the dispute.
                    source_type: nochurn
                    source_id: cancel_history
                    occurred_at: null
                    included: true
                    position: 1
                    file: null
                  - id: dev_MPZaQk8n7KM3cABs
                    object: dispute_evidence_item
                    kind: policy
                    title: Cancellation policy
                    content: Cancellation is available at any time in the app.
                    source_type: nochurn
                    source_id: policy
                    occurred_at: null
                    included: true
                    position: 2
                    file: null
                draft:
                  id: ddr_V9zGUkjGoVRGn4zG
                  object: dispute_draft
                  text: |-
                    We contest this subscription canceled dispute.

                    Evidence:
                    - No cancellation request: The customer never opened the cancellation flow before the dispute.
                    - Cancellation policy: Cancellation is available at any time in the app.
                  cited_item_ids:
                    - dev_JdC44XCLiiz2kLxu
                    - dev_MPZaQk8n7KM3cABs
                  model: template
                  status: draft
                  edited_by: null
                  edited_at: null
                  created_at: 2026-11-04T10:00:00Z
                charge_details:
                  paid_at: 2026-11-03T10:00:00Z
                  plan: Pro
                  invoice: null
                  period_end: null
                recommendation:
                  suggestion: null
                  reason: No evidence from a record. Set the cost of contesting in Chargebacks › Settings to compare it with the amount.
                  evidence_count: 0
                  amount_cents: 4900
                  contest_cost_cents: null
                  currency: eur
                  win_score: 65
                  expected_gain_cents: null
              schema:
                $ref: "#/components/schemas/DisputeDetail"
        "400":
          description: not an integer from 0 to 100
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - score
              properties:
                score:
                  type: integer
                  minimum: 0
                  maximum: 100
                  nullable: true
            example:
              score: 65
  /v1/fraud_warnings:
    get:
      summary: Early fraud warnings
      tags:
        - Chargebacks
      operationId: listFraudWarnings
      parameters:
        - name: state
          in: query
          required: false
          schema:
            type: string
            enum:
              - none
              - refunded
              - dismissed
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: listed
          content:
            application/json:
              example:
                object: list
                data:
                  - id: efw_UV9yBsfrkR2nKpAR
                    object: fraud_warning
                    livemode: false
                    provider: stripe
                    external_id: issfr_1
                    charge: ch_9
                    customer: null
                    customer_name: null
                    customer_email: null
                    customer_plan: null
                    amount_cents: null
                    currency: null
                    fraud_type: misc
                    actionable: true
                    action: none
                    acted_by: null
                    acted_at: null
                    note: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/FraudWarningList"
      x-audience: public
  /v1/fraud_warnings/{id}/refund:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Refund before the dispute (admin+, final)
      tags:
        - Chargebacks
      operationId: refundFraudWarning
      parameters: []
      responses:
        "200":
          description: refunded once
          content:
            application/json:
              example:
                id: efw_K5887AXzbU8YgLAM
                object: fraud_warning
                livemode: false
                provider: stripe
                external_id: issfr_1
                charge: ch_9
                customer: null
                customer_name: null
                customer_email: null
                customer_plan: null
                amount_cents: null
                currency: null
                fraud_type: misc
                actionable: true
                action: refunded
                acted_by: key_zA9EmYgpvf29Cd1D
                acted_at: 2026-11-04T10:00:00Z
                note: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/FraudWarning"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - confirm
              properties:
                confirm:
                  type: boolean
                  enum:
                    - true
            example:
              confirm: true
  /v1/fraud_warnings/{id}/dismiss:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Keep the payment
      tags:
        - Chargebacks
      operationId: dismissFraudWarning
      parameters: []
      responses:
        "200":
          description: dismissed
          content:
            application/json:
              example:
                id: efw_gw2bmQewwphGBtAv
                object: fraud_warning
                livemode: false
                provider: stripe
                external_id: issfr_1
                charge: ch_9
                customer: null
                customer_name: null
                customer_email: null
                customer_plan: null
                amount_cents: null
                currency: null
                fraud_type: misc
                actionable: true
                action: dismissed
                acted_by: key_TaFFSqDwmBtFwbL3
                acted_at: 2026-11-04T10:00:00Z
                note: Known customer
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/FraudWarning"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note:
                  type: string
            example:
              note: Known customer
  /v1/flows/{id}/email_preview:
    parameters:
      - name: id
        in: path
        description: flw_… or the kind of the default flow
        required: true
        schema:
          type: string
    post:
      summary: Preview an email of a flow
      tags:
        - Flows
      operationId: previewFlowEmail
      description: "Renders one email of the flow's draft for one language, from your text (saved, or `draft_content` being typed), the AI or NoChurn's template, with an example customer (fictional data) or one of your customers (`customer_id`, in the key's mode). Sends nothing, stores nothing. `source: ai` shows the last email the AI wrote for this step, or the template; `generate: true` asks the AI for a sample (20 a day). A restricted key needs flows:read."
      parameters: []
      responses:
        "200":
          description: the email, rendered
          content:
            application/json:
              example:
                object: email_preview
                step: email_1
                label: Email 1 · day 0
                language: fr
                source: template
                subject: Votre paiement n'est pas passé
                text: |-
                  Bonjour Alex,

                  Votre dernier paiement de €49.00 pour Acme n'est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.

                  Vous pouvez mettre à jour votre moyen de paiement en une minute : https://pay.nochurn.sh/example

                  Merci,
                  Acme
                html: '<p>Bonjour Alex,</p><p>Votre dernier paiement de €49.00 pour Acme n&#39;est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.</p><p>Vous pouvez mettre à jour votre moyen de paiement en une minute : <a href="https://pay.nochurn.sh/example">https://pay.nochurn.sh/example</a></p><p>Merci,<br>Acme</p>'
                ai_generated: false
                review:
                  state: auto_approved
                  issues: []
                warnings:
                  - "Links are examples: each customer gets their own link"
                customer:
                  id: null
                  name: Example customer
                  example: true
                sample: true
                template:
                  subject: Votre paiement n'est pas passé
                  body: |-
                    Bonjour {{first_name}},

                    Votre dernier paiement de {{amount}} pour {{company}} n'est pas passé. Cela arrive souvent quand une carte a expiré ou a été remplacée.

                    Vous pouvez mettre à jour votre moyen de paiement en une minute : {{card_link}}

                    Merci,
                    {{sender_name}}
                custom: null
                placeholders:
                  - first_name
                  - amount
                  - company
                  - card_link
                  - sender_name
                required_placeholders:
                  - card_link
                languages:
                  - en
                  - fr
                  - de
                  - it
                  - pt
                sources:
                  - custom
                  - ai
                  - template
              schema:
                $ref: "#/components/schemas/EmailPreview"
        "400":
          description: unknown step
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - step
              properties:
                step:
                  type: string
                  example: email_1
                  description: email_1…, card_removed, card_expiring, renewal_reminder, d30, d60, d90, review_follow_up
                language:
                  type: string
                  example: fr
                  description: "Default: the flow's first language."
                source:
                  type: string
                  enum:
                    - custom
                    - ai
                    - template
                  description: "Default: what the customer would get."
                draft_content:
                  type: object
                  required:
                    - subject
                    - body
                  properties:
                    subject:
                      type: string
                    body:
                      type: string
                  description: Your text being typed, not saved yet.
                customer_id:
                  type: string
                  description: "cst_…, cus_… or your id: fill the placeholders with this customer's data."
                generate:
                  type: boolean
                  description: "With source ai: ask the AI for a sample now."
            example:
              step: email_1
              language: fr
              source: template
        required: true
  /v1/inapp/resume:
    post:
      summary: Resume the paused subscription (in-app "Resume now")
      tags:
        - In-app
      operationId: resumeInappSubscription
      description: "From the paused wall or banner (DECISIONS 87): publishable key + customer token (its `subscription` claim names the subscription when there are several), or a secret key and ?customer=. Resumes the paused subscription at the provider (Stripe with an Idempotency-Key), writes a subscription.resumed event, and returns the new state (access full at once). 409 nothing_to_resume when nothing is paused (a second click included). Rate-limited like /v1/inapp/dismiss."
      parameters:
        - name: X-NoChurn-Customer-Token
          in: header
          required: false
          schema:
            type: string
        - name: Origin
          in: header
          required: false
          schema:
            type: string
        - name: customer
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: "resumed: access full again, an event, the access webhook restored"
          content:
            application/json:
              example:
                object: inapp_state
                customer: cst_33a3ErfXGkaBhxVd
                failing_since: null
                locale: en
                texts: null
                theme: null
                access: full
                access_reason: null
                banner:
                  show: false
                wall:
                  show: false
              schema:
                $ref: "#/components/schemas/InappState"
        "401":
          description: a token from another account never resumes this customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: nothing paused
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
  /v1/events:
    get:
      summary: List events, newest first
      tags:
        - Events
      operationId: listEvents
      parameters:
        - name: type
          in: query
          required: false
          schema:
            type: string
          description: Filter by event type, e.g. api_key.created
        - name: subject
          in: query
          required: false
          schema:
            type: string
          description: Filter by subject id, e.g. cst_… for a customer timeline
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
          description: "Cursor: the next_cursor of the previous page."
      responses:
        "200":
          description: events of the account, newest first
          content:
            application/json:
              example:
                object: list
                data:
                  - object: event
                    id: nevt_wr9AZ4FaA7NVz3Z7
                    type: api_key.created
                    subject:
                      type: api_key
                      id: key_xBrmAzbrotsgj2iT
                    reason: test
                    actor:
                      type: api_key
                      id: key_xBrmAzbrotsgj2iT
                      name: null
                    livemode: false
                    data: {}
                    created_at: 2026-11-04T10:00:00.000Z
                  - object: event
                    id: nevt_rRmSZmTKnHXN5bcN
                    type: account.created
                    subject: null
                    reason: null
                    actor:
                      type: system
                      id: null
                      name: null
                    livemode: false
                    data: {}
                    created_at: 2026-11-02T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/EventList"
        "400":
          description: invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/flow_notifications:
    get:
      summary: Slack messages and Intercom notes sent when a flow acts
      tags:
        - Flows
      operationId: getFlowNotifications
      description: "In the current mode. Slack: a message in a channel. Intercom: an internal note on the customer's contact (found by email), never a message to the customer. Each event is sent once per destination, retried with backoff and logged (Logs › webhook)."
      responses:
        "200":
          description: the settings
          content:
            application/json:
              example:
                object: flow_notifications
                livemode: false
                events:
                  - type: recovery.opened
                    flow: recovery
                  - type: recovery.recovered
                    flow: recovery
                  - type: recovery.lost
                    flow: recovery
                  - type: cancel_session.offer_accepted
                    flow: cancel
                  - type: cancel_session.completed
                    flow: cancel
                  - type: customer.flagged
                    flow: prevention
                  - type: winback.reactivated
                    flow: winback
                slack:
                  kind: slack
                  connected: true
                  integration_status: active
                  workspace: Globex
                  events: []
                  channel: "#alerts"
                  notes_access: null
                  last_delivery_at: null
                  last_error: null
                intercom:
                  kind: intercom_note
                  connected: false
                  integration_status: null
                  workspace: null
                  events: []
                  channel: null
                  notes_access: null
                  last_delivery_at: null
                  last_error: null
              schema:
                $ref: "#/components/schemas/FlowNotifications"
      x-audience: public
  /v1/flow_notifications/{kind}:
    parameters:
      - name: kind
        in: path
        enum:
          - slack
          - intercom_note
        required: true
        schema:
          type: string
    patch:
      summary: Choose the events sent to Slack (and its channel) or added as Intercom notes
      tags:
        - Flows
      operationId: updateFlowNotifications
      description: "Admins only. `events: []` turns the destination off. The integration must be connected to pick events."
      parameters: []
      responses:
        "200":
          description: saved
          content:
            application/json:
              example:
                object: flow_notifications
                livemode: false
                events:
                  - type: recovery.opened
                    flow: recovery
                  - type: recovery.recovered
                    flow: recovery
                  - type: recovery.lost
                    flow: recovery
                  - type: cancel_session.offer_accepted
                    flow: cancel
                  - type: cancel_session.completed
                    flow: cancel
                  - type: customer.flagged
                    flow: prevention
                  - type: winback.reactivated
                    flow: winback
                slack:
                  kind: slack
                  connected: true
                  integration_status: active
                  workspace: Globex
                  events:
                    - cancel_session.completed
                    - recovery.recovered
                  channel: "#retention"
                  notes_access: null
                  last_delivery_at: null
                  last_error: null
                intercom:
                  kind: intercom_note
                  connected: false
                  integration_status: null
                  workspace: null
                  events: []
                  channel: null
                  notes_access: null
                  last_delivery_at: null
                  last_error: null
              schema:
                $ref: "#/components/schemas/FlowNotifications"
        "409":
          description: not connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: an event that is not notified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - recovery.opened
                      - recovery.recovered
                      - recovery.lost
                      - cancel_session.offer_accepted
                      - cancel_session.completed
                      - customer.flagged
                      - winback.reactivated
                channel:
                  type: string
                  description: "Slack only: #channel or a channel id. A private channel needs the NoChurn app invited."
            example:
              events:
                - cancel_session.completed
                - recovery.recovered
              channel: "#retention"
        required: true
  /v1/flow_notifications/{kind}/test:
    parameters:
      - name: kind
        in: path
        enum:
          - slack
        required: true
        schema:
          type: string
    post:
      summary: Send a test message to the Slack channel now
      tags:
        - Flows
      operationId: testFlowNotifications
      responses:
        "201":
          description: posted
          content:
            application/json:
              example:
                object: webhook_delivery
                id: whd_4z6TAYS8bp3otSoZ
                livemode: false
                endpoint: whk_Px7rFYqisAcTaDdj
                destination: slack
                event: null
                type: test
                status: delivered
                attempts: 1
                next_attempt_at: null
                response_status: 200
                response_ms: null
                error: null
                delivered_at: 2026-11-04T10:00:00Z
                failed_at: null
                replay_of: null
                created_at: 2026-11-04T10:00:00Z
                payload:
                  id: nevt_test_f6a8e4b5b4ce
                  data: {}
                  type: test
                  object: event
                  created: 1793786400
                  livemode: false
                response_excerpt: "1.2"
              schema:
                $ref: "#/components/schemas/WebhookDeliveryDetail"
      x-audience: public
  /v1/flows:
    get:
      summary: List the 4 flows
      tags:
        - Flows
      operationId: listFlows
      responses:
        "200":
          description: prevention, recovery, cancel and win-back
          content:
            application/json:
              example:
                object: list
                data:
                  - object: flow
                    id: flw_1K3ms53XtrQohYHb
                    kind: prevention
                    key: default
                    name: Prevention flow
                    status: draft
                    holdout_percent: 5
                    test_version: null
                    live_version: null
                    unpublished_changes: []
                    mode_status:
                      livemode: false
                      state: draft
                      version: null
                      published_at: null
                      unpublished_changes: []
                    draft_updated_at: 2026-11-04T10:00:00.000Z
                    paused_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                  - object: flow
                    id: flw_pqMp6g74MZoresPY
                    kind: recovery
                    key: default
                    name: Recovery flow
                    status: draft
                    holdout_percent: 0
                    test_version: null
                    live_version: null
                    unpublished_changes: []
                    mode_status:
                      livemode: false
                      state: draft
                      version: null
                      published_at: null
                      unpublished_changes: []
                    draft_updated_at: 2026-11-04T10:00:00.000Z
                    paused_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                  - object: flow
                    id: flw_NZ3Jng8vP1kfQmtf
                    kind: cancel
                    key: default
                    name: Cancel flow
                    status: draft
                    holdout_percent: 5
                    test_version: null
                    live_version: null
                    unpublished_changes: []
                    mode_status:
                      livemode: false
                      state: draft
                      version: null
                      published_at: null
                      unpublished_changes: []
                    draft_updated_at: 2026-11-04T10:00:00.000Z
                    paused_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                  - object: flow
                    id: flw_rVqAhXgThA4dJQdJ
                    kind: winback
                    key: default
                    name: Win-back flow
                    status: draft
                    holdout_percent: 5
                    test_version: null
                    live_version: null
                    unpublished_changes: []
                    mode_status:
                      livemode: false
                      state: draft
                      version: null
                      published_at: null
                      unpublished_changes: []
                    draft_updated_at: 2026-11-04T10:00:00.000Z
                    paused_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                    updated_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/FlowList"
      x-audience: public
  /v1/flows/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
        description: "flw_… or the kind of the default flow: prevention, recovery, cancel, winback."
    get:
      summary: Retrieve a flow with its draft
      tags:
        - Flows
      operationId: getFlow
      responses:
        "200":
          description: the flow
          content:
            application/json:
              example:
                object: flow
                id: flw_ANSL3Lgcmq6LE2og
                kind: recovery
                key: default
                name: Recovery flow
                status: draft
                holdout_percent: 0
                test_version: null
                live_version: null
                unpublished_changes: []
                mode_status:
                  livemode: false
                  state: draft
                  version: null
                  published_at: null
                  unpublished_changes: []
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  inapp:
                    wall:
                      enabled: true
                      after_days: 7
                    banner:
                      enabled: true
                      audience: admins
                      dismiss_hours: 24
                    annual_grace_days: 14
                  emails:
                    - day: 0
                    - day: 4
                    - day: 10
                  retries:
                    then: cancel_at_period_end_and_winback
                    rules:
                      - timing: after_payday
                        attempts: 4
                        decline_code: insufficient_funds
                      - attempts: 0
                        email_now: true
                        decline_code: expired_card
                      - days:
                          - 3
                          - 5
                          - 7
                        attempts: 3
                        decline_code: do_not_honor
                      - stop: true
                        email: false
                        attempts: 0
                        decline_code: fraudulent
                    max_days: 14
                  send_window:
                    audience: null
                    quiet_hours: null
                    respect_customer_timezone: null
                  email_settings:
                    tone: friendly
                    review: auto_check
                    ai_writes: true
                    languages:
                      - en
                      - fr
                      - de
                      - it
                      - pt
                  card_update_page:
                    methods:
                      - card
                      - apple_pay
                      - google_pay
                      - sepa_debit
                    link_valid_days: 14
                    pay_open_invoice: true
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
        "404":
          description: unknown flow
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
    patch:
      summary: Edit the draft
      tags:
        - Flows
      operationId: updateFlow
      description: A section sent replaces that section of the draft. Changes run only once published.
      parameters: []
      responses:
        "200":
          description: draft updated, unpublished changes listed
          content:
            application/json:
              example:
                object: flow
                id: flw_YJEtbe7bMLQa255h
                kind: recovery
                key: default
                name: Recovery flow
                status: draft
                holdout_percent: 5
                test_version: null
                live_version: null
                unpublished_changes:
                  - retries
                mode_status:
                  livemode: false
                  state: draft
                  version: null
                  published_at: null
                  unpublished_changes:
                    - retries
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  inapp:
                    wall:
                      enabled: true
                      after_days: 7
                    banner:
                      enabled: true
                      audience: admins
                      dismiss_hours: 24
                    annual_grace_days: 14
                  emails:
                    - day: 0
                    - day: 4
                    - day: 10
                  retries:
                    max_days: 10
                  send_window:
                    audience: null
                    quiet_hours: null
                    respect_customer_timezone: null
                  email_settings:
                    tone: friendly
                    review: auto_check
                    ai_writes: true
                    languages:
                      - en
                      - fr
                      - de
                      - it
                      - pt
                  card_update_page:
                    methods:
                      - card
                      - apple_pay
                      - google_pay
                      - sepa_debit
                    link_valid_days: 14
                    pay_open_invoice: true
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
        "400":
          description: unknown section
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                draft:
                  type: object
                  additionalProperties: true
                name:
                  type: string
                holdout_percent:
                  type: integer
                  minimum: 0
                  maximum: 50
            example:
              draft:
                retries:
                  max_days: 10
              holdout_percent: 5
        required: true
  /v1/flows/{id}/publish:
    post:
      summary: Publish the draft to test or live mode
      tags:
        - Flows
      operationId: publishFlow
      description: 'to: test is "Try in test mode"; to: live is "Publish" (both modes then run this version).'
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: published
          content:
            application/json:
              example:
                object: flow
                id: flw_wrgKan8xHjAdgH4k
                kind: cancel
                key: default
                name: Cancel flow
                status: live
                holdout_percent: 5
                test_version:
                  id: flv_hPABNrYmFDsYS7G8
                  number: 1
                  published_at: 2026-11-04T10:00:00.000Z
                  published_to: live
                live_version:
                  id: flv_hPABNrYmFDsYS7G8
                  number: 1
                  published_at: 2026-11-04T10:00:00.000Z
                  published_to: live
                unpublished_changes: []
                mode_status:
                  livemode: false
                  state: published
                  version: 1
                  published_at: 2026-11-04T10:00:00.000Z
                  unpublished_changes: []
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  pause:
                    inapp: none
                  steps:
                    - reason
                    - offer
                    - feedback
                    - confirmation
                  texts: {}
                  design:
                    format: modal
                    preset: light
                    tokens: {}
                    custom_css: ""
                  reasons:
                    - key: too_expensive
                      labels:
                        de: Es ist zu teuer
                        en: It's too expensive
                        fr: C'est trop cher
                        it: È troppo caro
                        pt: É demasiado caro
                      offer_id: null
                    - key: missing_feature
                      labels:
                        de: Eine Funktion fehlt
                        en: A feature is missing
                        fr: Il manque une fonctionnalité
                        it: Manca una funzionalità
                        pt: Falta uma funcionalidade
                      offer_id: null
                    - key: not_using
                      labels:
                        de: Ich nutze es zu wenig
                        en: I don't use it enough
                        fr: Je ne l'utilise pas assez
                        it: Non lo uso abbastanza
                        pt: Não o uso o suficiente
                      offer_id: null
                    - key: switching
                      labels:
                        de: Ich wechsle zu einem anderen Tool
                        en: I'm switching to another tool
                        fr: Je passe à un autre outil
                        it: Passo a un altro strumento
                        pt: Vou mudar para outra ferramenta
                      offer_id: null
                    - key: temporary
                      labels:
                        de: Ich brauche nur eine Pause
                        en: I only need a break
                        fr: J'ai juste besoin d'une pause
                        it: Ho solo bisogno di una pausa
                        pt: Só preciso de uma pausa
                      offer_id: null
                    - key: project_finished
                      labels:
                        de: Mein Projekt ist abgeschlossen
                        en: My project is finished
                        fr: Mon projet est terminé
                        it: Il mio progetto è finito
                        pt: O meu projeto terminou
                      offer_id: null
                    - key: other
                      labels:
                        de: Etwas anderes
                        en: Something else
                        fr: Autre chose
                        it: Altro
                        pt: Outra coisa
                      offer_id: null
                      free_text: true
                  feedback:
                    enabled: true
                    required: false
                  cancel_mode: at_period_end
                  review_requests:
                    enabled: false
                    happy_reasons:
                      - project_finished
                      - too_expensive
                      - temporary
                    follow_up_email_days: 2
                  offers_once_per_customer_per_year: true
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - to
              properties:
                to:
                  type: string
                  enum:
                    - test
                    - live
            example:
              to: live
        required: true
  /v1/flows/{id}/discard:
    post:
      summary: Discard the unpublished changes
      tags:
        - Flows
      operationId: discardFlowDraft
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: draft back to the last version
          content:
            application/json:
              example:
                object: flow
                id: flw_qUMZ9KGuvjKnxmww
                kind: recovery
                key: default
                name: Recovery flow
                status: draft
                holdout_percent: 0
                test_version: null
                live_version: null
                unpublished_changes: []
                mode_status:
                  livemode: false
                  state: draft
                  version: null
                  published_at: null
                  unpublished_changes: []
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  retries:
                    rules:
                      - decline_code: insufficient_funds
                        attempts: 4
                        timing: after_payday
                      - decline_code: expired_card
                        attempts: 0
                        email_now: true
                      - decline_code: do_not_honor
                        attempts: 3
                        days:
                          - 3
                          - 5
                          - 7
                      - decline_code: fraudulent
                        attempts: 0
                        stop: true
                        email: false
                    max_days: 14
                    then: cancel_at_period_end_and_winback
                  emails:
                    - day: 0
                    - day: 4
                    - day: 10
                  email_settings:
                    ai_writes: true
                    tone: friendly
                    review: auto_check
                    languages:
                      - en
                      - fr
                      - de
                      - it
                      - pt
                  card_update_page:
                    link_valid_days: 14
                    pay_open_invoice: true
                    methods:
                      - card
                      - apple_pay
                      - google_pay
                      - sepa_debit
                  inapp:
                    banner:
                      enabled: true
                      audience: admins
                      dismiss_hours: 24
                    wall:
                      enabled: true
                      after_days: 7
                    annual_grace_days: 14
                  send_window:
                    audience: null
                    respect_customer_timezone: null
                    quiet_hours: null
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
      x-audience: public
  /v1/flows/{id}/versions:
    get:
      summary: List published versions, newest first
      tags:
        - Flows
      operationId: listFlowVersions
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: versions
          content:
            application/json:
              example:
                object: list
                data:
                  - object: flow_version
                    id: flv_uLhBgP6hNvvgKQbm
                    flow: flw_Wwa1oeVCBHng5M6m
                    number: 2
                    config:
                      inapp:
                        wall:
                          enabled: true
                          after_days: 7
                        banner:
                          enabled: true
                          audience: admins
                          dismiss_hours: 24
                        annual_grace_days: 14
                      emails:
                        - day: 0
                        - day: 4
                        - day: 10
                      retries:
                        then: cancel_at_period_end_and_winback
                        rules:
                          - timing: after_payday
                            attempts: 4
                            decline_code: insufficient_funds
                          - attempts: 0
                            email_now: true
                            decline_code: expired_card
                          - days:
                              - 3
                              - 5
                              - 7
                            attempts: 3
                            decline_code: do_not_honor
                          - stop: true
                            email: false
                            attempts: 0
                            decline_code: fraudulent
                        max_days: 14
                      send_window:
                        audience: null
                        quiet_hours: null
                        respect_customer_timezone: null
                      email_settings:
                        tone: friendly
                        review: auto_check
                        ai_writes: true
                        languages:
                          - en
                          - fr
                          - de
                          - it
                          - pt
                      card_update_page:
                        methods:
                          - card
                          - apple_pay
                          - google_pay
                          - sepa_debit
                        link_valid_days: 14
                        pay_open_invoice: true
                    holdout_percent: 0
                    published_to: test
                    published_by: system
                    published_at: 2026-11-04T10:00:00.000Z
                  - object: flow_version
                    id: flv_VGm8keAZNkfA8TPw
                    flow: flw_Wwa1oeVCBHng5M6m
                    number: 1
                    config:
                      inapp:
                        wall:
                          enabled: true
                          after_days: 7
                        banner:
                          enabled: true
                          audience: admins
                          dismiss_hours: 24
                        annual_grace_days: 14
                      emails:
                        - day: 0
                        - day: 4
                        - day: 10
                      retries:
                        then: cancel_at_period_end_and_winback
                        rules:
                          - timing: after_payday
                            attempts: 4
                            decline_code: insufficient_funds
                          - attempts: 0
                            email_now: true
                            decline_code: expired_card
                          - days:
                              - 3
                              - 5
                              - 7
                            attempts: 3
                            decline_code: do_not_honor
                          - stop: true
                            email: false
                            attempts: 0
                            decline_code: fraudulent
                        max_days: 14
                      send_window:
                        audience: null
                        quiet_hours: null
                        respect_customer_timezone: null
                      email_settings:
                        tone: friendly
                        review: auto_check
                        ai_writes: true
                        languages:
                          - en
                          - fr
                          - de
                          - it
                          - pt
                      card_update_page:
                        methods:
                          - card
                          - apple_pay
                          - google_pay
                          - sepa_debit
                        link_valid_days: 14
                        pay_open_invoice: true
                    holdout_percent: 0
                    published_to: test
                    published_by: system
                    published_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/FlowVersionList"
      x-audience: public
  /v1/flows/{id}/versions/{number}/restore:
    post:
      summary: Put an older version back in the draft
      tags:
        - Flows
      operationId: restoreFlowVersion
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: number
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: restored in the draft (publish to apply)
          content:
            application/json:
              example:
                object: flow
                id: flw_bHzwVnLVo1KxDxu6
                kind: recovery
                key: default
                name: Recovery flow
                status: test
                holdout_percent: 0
                test_version:
                  id: flv_ymeANEcX3A1TunN7
                  number: 2
                  published_at: 2026-11-04T10:00:00.000Z
                  published_to: test
                live_version: null
                unpublished_changes:
                  - retries
                mode_status:
                  livemode: false
                  state: published
                  version: 2
                  published_at: 2026-11-04T10:00:00.000Z
                  unpublished_changes:
                    - retries
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  inapp:
                    wall:
                      enabled: true
                      after_days: 7
                    banner:
                      enabled: true
                      audience: admins
                      dismiss_hours: 24
                    annual_grace_days: 14
                  emails:
                    - day: 0
                    - day: 4
                    - day: 10
                  retries:
                    then: cancel_at_period_end_and_winback
                    rules:
                      - timing: after_payday
                        attempts: 4
                        decline_code: insufficient_funds
                      - attempts: 0
                        email_now: true
                        decline_code: expired_card
                      - days:
                          - 3
                          - 5
                          - 7
                        attempts: 3
                        decline_code: do_not_honor
                      - stop: true
                        email: false
                        attempts: 0
                        decline_code: fraudulent
                    max_days: 14
                  send_window:
                    audience: null
                    quiet_hours: null
                    respect_customer_timezone: null
                  email_settings:
                    tone: friendly
                    review: auto_check
                    ai_writes: true
                    languages:
                      - en
                      - fr
                      - de
                      - it
                      - pt
                  card_update_page:
                    methods:
                      - card
                      - apple_pay
                      - google_pay
                      - sepa_debit
                    link_valid_days: 14
                    pay_open_invoice: true
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
      x-audience: public
  /v1/flows/{id}/pause:
    post:
      summary: Pause a flow (no new run)
      tags:
        - Flows
      operationId: pauseFlow
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: paused
          content:
            application/json:
              example:
                object: flow
                id: flw_owZ77WgHmyWTS5hn
                kind: winback
                key: default
                name: Win-back flow
                status: paused
                holdout_percent: 5
                test_version: null
                live_version: null
                unpublished_changes: []
                mode_status:
                  livemode: false
                  state: paused
                  version: null
                  published_at: null
                  unpublished_changes: []
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  offer:
                    label: 1 month free
                    promo_code: null
                  stop_on:
                    - reply
                    - resubscribe
                    - unsubscribe
                  sequence:
                    - day: 30
                      kind: d30
                    - day: 60
                      kind: d60
                    - day: 90
                      kind: d90
                  never_after:
                    - refund
                    - fraud
                    - gdpr_erasure
                  send_window:
                    audience: null
                    quiet_hours: null
                    respect_customer_timezone: null
                  resubscribe_url: null
                  max_churn_age_days: 7
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
        "403":
          description: restricted key with flows:read only
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/flows/{id}/resume:
    post:
      summary: Resume a paused flow
      tags:
        - Flows
      operationId: resumeFlow
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: resumed
          content:
            application/json:
              example:
                object: flow
                id: flw_RnhboVHZYcVRnCsL
                kind: winback
                key: default
                name: Win-back flow
                status: draft
                holdout_percent: 5
                test_version: null
                live_version: null
                unpublished_changes: []
                mode_status:
                  livemode: false
                  state: draft
                  version: null
                  published_at: null
                  unpublished_changes: []
                draft_updated_at: 2026-11-04T10:00:00.000Z
                paused_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                draft:
                  offer:
                    label: 1 month free
                    promo_code: null
                  stop_on:
                    - reply
                    - resubscribe
                    - unsubscribe
                  sequence:
                    - day: 30
                      kind: d30
                    - day: 60
                      kind: d60
                    - day: 90
                      kind: d90
                  never_after:
                    - refund
                    - fraud
                    - gdpr_erasure
                  send_window:
                    audience: null
                    quiet_hours: null
                    respect_customer_timezone: null
                  resubscribe_url: null
                  max_churn_age_days: 7
                logos: []
              schema:
                $ref: "#/components/schemas/FlowDetail"
      x-audience: public
  /v1/flows/{id}/runs:
    get:
      summary: Customers in this flow (current mode)
      tags:
        - Flows
      operationId: listFlowRuns
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: state
          in: query
          required: false
          schema:
            type: string
            enum:
              - active
              - succeeded
              - failed
              - stopped
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: runs of the mode, with their customer
          content:
            application/json:
              example:
                object: list
                data:
                  - object: flow_run
                    id: run_DDiKSSzFHtYhREuC
                    livemode: false
                    flow: flw_V7HicHjaunR9yXum
                    version: 1
                    customer:
                      id: cst_77ZMsUw9nrd8gjCi
                      name: Lena
                      email: customer1@acme-corp.dev
                      mrr_cents: 0
                      currency: null
                    subject: null
                    holdout: false
                    state: active
                    current_step: entered
                    outcome: null
                    outcome_at: null
                    outcome_mrr_cents: null
                    outcome_cents: null
                    currency: null
                    confirmation: null
                    stop_reason: null
                    started_at: 2026-11-04T10:00:00.000Z
                    ended_at: null
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/FlowRunList"
      x-audience: public
  /v1/inapp/state:
    get:
      summary: "What the customer's app shows: payment banner and wall"
      tags:
        - In-app
      operationId: getInappState
      description: "From the browser: publishable key + customer token (HS256 { kind, value, exp } signed with your widget secret) + an allowed Origin. From a server: secret key and ?customer=."
      parameters:
        - name: X-NoChurn-Customer-Token
          in: header
          required: false
          schema:
            type: string
        - name: Origin
          in: header
          required: false
          schema:
            type: string
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: locale
          in: query
          required: false
          enum:
            - en
            - fr
            - de
            - it
            - pt
          description: "Texts language; default: the customer's, then the account's.:

            \ * `en`\ 

            \ * `fr`\ 

            \ * `de`\ 

            \ * `it`\ 

            \ * `pt`\ 

            \ "
          schema:
            type: string
      responses:
        "200":
          description: "a failing payment: banner on, wall after 7 days"
          content:
            application/json:
              example:
                object: inapp_state
                customer: cst_vDH4Ubcpyj5pzVs1
                failing_since: 2026-11-04T10:00:00Z
                locale: fr
                texts:
                  banner_text: Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.
                  banner_cta: Payer maintenant
                  dismiss: Masquer
                  banner_days_left: Plus que {{days}} jours avant la suspension de votre compte.
                  banner_last_day: Dernier jour avant la suspension de votre compte.
                  wall_title: Votre compte est suspendu
                  wall_text: Votre compte est suspendu jusqu'au paiement de €49.00.
                  wall_cta: Mettre à jour le moyen de paiement
                  pay_cta: Mettre à jour la carte et payer €49.00
                  amount_due: Montant dû
                  card_expired: Expirée {{exp}}
                  decline_expired: Carte expirée
                  decline_insufficient_funds: Fonds insuffisants
                  decline_authentication_required: Votre banque demande de valider le paiement
                  decline_declined: Refusée par votre banque
                  checking_payment: Vérification du paiement…
                  back_in_title: Vous revoilà
                  back_in_text: Paiement reçu. Merci !
                  data_safe: Vos données sont en sécurité. Rien n'est supprimé.
                  paused_banner_text: Votre abonnement est en pause jusqu'au {{date}}.
                  paused_title: Votre abonnement est en pause
                  paused_text: Aucun prélèvement pendant la pause. Vos données sont conservées.
                  paused_resumes: Reprise automatique dans {{days}} jours, à €49.00.
                  resume_cta: Reprendre maintenant
                  resuming: Reprise…
                  resumed_title: Bon retour
                  resumed_text: Votre abonnement est de nouveau actif.
                  cancel_now: Résilier mon abonnement
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                access: full
                access_reason: null
                banner:
                  show: true
                  kind: payment_failed
                  audience: admins
                  text: Votre dernier paiement de €49.00 n'est pas passé. Mettez à jour votre moyen de paiement pour garder votre compte.
                  cta_url: http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R
                  cta_label: Payer maintenant
                  dismiss_label: Masquer
                  dismissible_for_hours: 24
                  restricts_at: 2026-11-11T10:00:00Z
                  days_left: 7
                  details:
                    kind: payment_failed
                    amount_due_cents: 4900
                    currency: eur
                    invoice_due_at: null
                    payment_method: null
                    decline: expired
                wall:
                  show: false
                  kind: payment_failed
                  text: Votre compte est suspendu jusqu'au paiement de €49.00.
                  cta_url: http://localhost:3000/c/okIJi02RYcm_ylJZATU-2KwNQpxXw64R
                  cta_label: Mettre à jour le moyen de paiement
                  shows_after_days: 7
                  cancel:
                    label: Résilier mon abonnement
                    fallback_url: http://localhost:3000/k/pk_test_bCx4srPPXRCaL7ykidyP3qc498kijZxq
                  details: null
              schema:
                $ref: "#/components/schemas/InappState"
        "401":
          description: a token signed with another secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: an origin that is not allowed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
  /v1/inapp/dismiss:
    post:
      summary: Hide the banner for 24 hours
      tags:
        - In-app
      operationId: dismissInappBanner
      parameters:
        - name: X-NoChurn-Customer-Token
          in: header
          required: false
          schema:
            type: string
        - name: Origin
          in: header
          required: false
          schema:
            type: string
      responses:
        "200":
          description: hidden; the wall cannot be dismissed
          content:
            application/json:
              example:
                object: inapp_state
                customer: cst_vCz9mjyDe3XdmdTJ
                failing_since: 2026-11-04T10:00:00Z
                locale: en
                texts:
                  banner_text: Your last payment of €49.00 didn't go through. Update your payment method to keep your account.
                  banner_cta: Update payment method
                  dismiss: Dismiss
                  banner_days_left: "{{days}} days left before your account is on hold."
                  banner_last_day: Last day before your account is on hold.
                  wall_title: Your account is on hold
                  wall_text: Your account is on hold until the payment of €49.00 goes through.
                  wall_cta: Update payment method
                  pay_cta: Update card and pay €49.00
                  amount_due: Amount due
                  card_expired: Expired {{exp}}
                  decline_expired: Card expired
                  decline_insufficient_funds: Insufficient funds
                  decline_authentication_required: Your bank needs you to approve the payment
                  decline_declined: Declined by your bank
                  checking_payment: Checking your payment…
                  back_in_title: You're back in
                  back_in_text: Payment received. Thanks!
                  data_safe: Your data is safe. Nothing is deleted.
                  paused_banner_text: Your plan is paused until {{date}}.
                  paused_title: Your plan is paused
                  paused_text: No charges while it's paused. Your data is kept.
                  paused_resumes: Resumes by itself in {{days}} days, at €49.00.
                  resume_cta: Resume now
                  resuming: Resuming…
                  resumed_title: Welcome back
                  resumed_text: Your plan is active again.
                  cancel_now: Cancel my subscription
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Account 1
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                access: full
                access_reason: null
                banner:
                  show: false
                  kind: payment_failed
                  audience: admins
                  text: Your last payment of €49.00 didn't go through. Update your payment method to keep your account.
                  cta_url: http://localhost:3000/c/XjLe-todmcu_jXSv4LVBfduidG16OE8l
                  cta_label: Update payment method
                  dismiss_label: Dismiss
                  dismissible_for_hours: 24
                  restricts_at: 2026-11-11T10:00:00Z
                  days_left: 7
                  details: null
                wall:
                  show: false
                  kind: payment_failed
                  text: Your account is on hold until the payment of €49.00 goes through.
                  cta_url: http://localhost:3000/c/XjLe-todmcu_jXSv4LVBfduidG16OE8l
                  cta_label: Update payment method
                  shows_after_days: 7
                  cancel:
                    label: Cancel my subscription
                    fallback_url: http://localhost:3000/k/pk_test_BBBxnJuz1rBMZ7B1jEkAwn4sKy8napzG
                  details: null
              schema:
                $ref: "#/components/schemas/InappState"
      x-audience: browser
  /v1/integrations:
    get:
      summary: Connected tools
      tags:
        - Integrations
      operationId: listIntegrations
      responses:
        "200":
          description: credentials are never returned
          content:
            application/json:
              example:
                object: list
                has_more: false
                next_cursor: null
                data:
                  - object: integration
                    id: itg_GryteF3meCbHffEf
                    kind: slack
                    status: active
                    external_account: Acme
                    config: {}
                    last_synced_at: null
                    error: null
                    conversations_count: null
                    created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/IntegrationList"
      x-audience: public
    post:
      summary: Connect Crisp (website or plugin token) or Gorgias (REST API key)
      tags:
        - Integrations
      operationId: connectCrisp
      description: "Crisp: `identifier`, `key` (a website token from Settings › Workspace Settings › Advanced configuration › API Token, or a Marketplace plugin token), `website_id`. Gorgias: `domain` (the part before .gorgias.com), `email` and `api_key` (Settings › Account › REST API). Read only; credentials are encrypted and never returned."
      parameters: []
      responses:
        "201":
          description: connected (read only)
          content:
            application/json:
              example:
                object: integration
                id: itg_PTnp2N7f2MPagEv6
                kind: crisp
                status: active
                external_account: Acme
                config: {}
                last_synced_at: null
                error: null
                conversations_count: 0
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Integration"
        "400":
          description: Crisp refused the credentials or does not know the website
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required:
                    - kind
                    - identifier
                    - key
                    - website_id
                  properties:
                    kind:
                      type: string
                      enum:
                        - crisp
                    identifier:
                      type: string
                    key:
                      type: string
                    website_id:
                      type: string
                - type: object
                  required:
                    - kind
                    - domain
                    - email
                    - api_key
                  properties:
                    kind:
                      type: string
                      enum:
                        - gorgias
                    domain:
                      type: string
                      example: acme
                    email:
                      type: string
                    api_key:
                      type: string
            example:
              kind: crisp
              identifier: id_test
              key: key_test
              website_id: 8c1f3f52-2a8a-4d6b-9a0e-6f1f2c3d4e5f
        required: true
  /v1/integrations/{kind}/authorize:
    parameters:
      - name: kind
        in: path
        schema:
          type: string
          enum:
            - intercom
            - zendesk
            - hubspot
            - helpscout
            - slack
        required: true
    post:
      summary: OAuth URL of a support tool or Slack
      tags:
        - Integrations
      operationId: authorizeIntegration
      description: "The dashboard only redirects to `url`. Zendesk needs the account's `subdomain` (connect as a Zendesk admin). Intercom takes an optional `region` (`us`, `eu`, `au`: where the workspace is hosted; `us` by default) for its authorization host."
      parameters: []
      responses:
        "200":
          description: the URL
          content:
            application/json:
              example:
                object: authorize_url
                kind: intercom
                url: https://app.intercom.com/oauth?client_id=ic_test&state=eyJfcmFpbHMiOnsiZGF0YSI6eyJhY2NvdW50X2lkIjoiYWNjX3F2YXRjSlR6d25DWjRaWUoiLCJ1c2VyX2lkIjpudWxsLCJraW5kIjoiaW50ZXJjb20iLCJub25jZSI6IjE2OWU1ZDQ5MTcxNGZjYmQifSwiZXhwIjoiMjAyNi0xMS0wNFQxMDozMDowMC4wMDBaIiwicHVyIjoiaW50ZWdyYXRpb24ifX0%3D--7d97ba0133a2d258517598968809f921d24c10e0
              schema:
                $ref: "#/components/schemas/IntegrationAuthorizeUrl"
        "400":
          description: unknown region
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subdomain:
                  type: string
                region:
                  type: string
                  enum:
                    - us
                    - eu
                    - au
            example: {}
  /v1/integrations/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Slack channel
      tags:
        - Integrations
      operationId: updateIntegration
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: integration
                id: itg_WFTBKq2vf2R8QZMo
                kind: slack
                status: active
                external_account: Acme
                config:
                  channel: "#retention"
                last_synced_at: null
                error: null
                conversations_count: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Integration"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                config:
                  type: object
                  properties:
                    channel:
                      type: string
            example:
              config:
                channel: "#retention"
        required: true
    delete:
      summary: Disconnect
      tags:
        - Integrations
      operationId: disconnectIntegration
      responses:
        "200":
          description: credentials erased
          content:
            application/json:
              example:
                object: integration
                id: itg_tRyqFBV8V6BgHf6d
                kind: slack
                status: revoked
                external_account: Acme
                config: {}
                last_synced_at: null
                error: null
                conversations_count: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Integration"
      x-audience: public
  /v1/integrations/{id}/sync:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Sync now (support tools)
      tags:
        - Integrations
      operationId: syncIntegration
      responses:
        "202":
          description: queued
          content:
            application/json:
              example:
                object: integration
                id: itg_Pa34cY9hMexK8b2u
                kind: intercom
                status: active
                external_account: Acme
                config: {}
                last_synced_at: null
                error: null
                conversations_count: 0
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Integration"
        "409":
          description: not connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/feedback:
    get:
      summary: Feedback themes and latest answers
      tags:
        - Data & AI
      operationId: getFeedback
      responses:
        "200":
          description: nothing yet
          content:
            application/json:
              example:
                object: feedback
                answers_90d: 0
                run: null
                items: []
              schema:
                $ref: "#/components/schemas/Feedback"
      x-audience: public
  /v1/feedback/refresh:
    post:
      summary: Theme again (once an hour)
      tags:
        - Data & AI
      operationId: refreshFeedback
      responses:
        "202":
          description: queued
          content:
            application/json:
              example:
                object: feedback_refresh
                queued: true
              schema:
                $ref: "#/components/schemas/FeedbackRefresh"
        "429":
          description: already requested this hour (counted from the click)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/customers/{id}/insight:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: AI analysis of a customer
      tags:
        - Customers
      operationId: analyzeCustomer
      description: "Without name, email or sensitive attributes; every number comes from the profile. Fresh for 7 days unless an important event for the customer comes after it (the profile's ai_analysis_status): a fresh analysis is returned without an AI call. One at a time per customer, 200 AI calls a day per account."
      responses:
        "200":
          description: the analysis
          content:
            application/json:
              example:
                object: customer_insight
                id: cai_Dbnz8NirwtHM5xPz
                summary: On the Pro plan.
                signals:
                  - Pro
                next_step: Check in
                sources:
                  - custom_data
                computed_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/CustomerInsight"
        "422":
          description: not enough activity for an analysis (insufficient_data, no AI call)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/live_requests:
    post:
      summary: Ask a human to do something in live mode (AI apps)
      tags:
        - AI apps
      operationId: createLiveRequest
      description: "OAuth tokens only (configure scope, plus the scope the operation needs in test mode: sandbox for customers, usage, subscriptions and payments). AI apps never change live mode directly: they ask for one operation (action) with its params (one schema per operation, see LiveRequestCreate) and give approval_url to an owner or admin. NoChurn checks the params, looks the target up in live mode, writes its own summary and captures what must still hold at approval (precondition). Nothing runs until a human approves it in the dashboard; it then runs in live mode as that human. Asked in any mode (the NoChurn-Mode header is ignored here). The same pending request asked again is returned as is. Never possible: the team, AI app access, 2FA, NoChurn's own plan, editing or revoking existing keys, deleting the account."
      parameters: []
      responses:
        "201":
          description: pending, with its approval link
          content:
            application/json:
              example:
                object: live_request
                id: lrq_7y3fem7wpcHdUx1s
                action: create_live_api_key
                title: Create a live API key
                summary: Create a live secret (nc_live_…) API key named “Production server”, full access
                params:
                  name: Production server
                  kind: secret
                  access: full
                  permissions: {}
                precondition: null
                reason: Integration verified in test mode
                status: pending
                requested_by:
                  type: ai_agent
                  name: Claude Code
                  grant_id: ogr_A9AUMTA8qUFrqLLZ
                approval_url: http://localhost:5173/live-requests/lrq_7y3fem7wpcHdUx1s?account=acc_itN4DgVRL8A9caPs
                decided_by: null
                decided_at: null
                result: {}
                error: null
                api_key: null
                response: null
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/LiveRequest"
        "400":
          description: unknown action
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: an API key (act in live mode directly instead)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LiveRequestCreate"
            example:
              action: create_live_api_key
              params:
                name: Production server
              reason: Integration verified in test mode
        required: true
    get:
      summary: List live requests
      tags:
        - AI apps
      operationId: listLiveRequests
      description: An AI app sees its own requests; the dashboard sees every request of the account.
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - pending
              - approved
              - rejected
              - failed
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the requests
          content:
            application/json:
              example:
                object: list
                data:
                  - object: live_request
                    id: lrq_x1S3mBZzebRmeJkX
                    action: set_billing_source
                    title: Change the billing source
                    summary: Set the billing source to your own billing (API) (test and live modes)
                    params:
                      billing_source: api
                    precondition:
                      kind: current_value
                      object: billing_source
                      id: null
                      expected: stripe
                      status: holds
                      description: The billing source must still be stripe.
                    reason: Ready for production
                    status: pending
                    requested_by:
                      type: ai_agent
                      name: Claude Code
                      grant_id: ogr_9Gp8DnTNkPEsTAAU
                    approval_url: http://localhost:5173/live-requests/lrq_x1S3mBZzebRmeJkX?account=acc_n31kFwAhaVTBRdpZ
                    decided_by: null
                    decided_at: null
                    result: {}
                    error: null
                    api_key: null
                    response: null
                    expires_at: 2026-11-11T10:00:00.000Z
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/LiveRequestList"
      x-audience: public
  /v1/live_requests/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Retrieve a live request
      tags:
        - AI apps
      operationId: getLiveRequest
      description: AI apps poll it to know whether a human approved.
      responses:
        "200":
          description: the request
          content:
            application/json:
              example:
                object: live_request
                id: lrq_a29dDA8tYCJq8KTP
                action: publish_flow_live
                title: Publish a flow to live mode
                summary: Publish the current draft of Cancel flow to live mode (customers in live mode get it)
                params:
                  flow_id: flw_K13CEko818VgKSAc
                  flow_kind: cancel
                  flow_name: Cancel flow
                precondition:
                  kind: draft_digest
                  object: flow
                  id: flw_K13CEko818VgKSAc
                  expected: null
                  status: holds
                  description: The flow draft must be the one the app asked to publish.
                reason: Ready for production
                status: pending
                requested_by:
                  type: ai_agent
                  name: Claude Code
                  grant_id: ogr_CxuaJ9mcH429DzBw
                approval_url: http://localhost:5173/live-requests/lrq_a29dDA8tYCJq8KTP?account=acc_ptg1cVcxU237ywmi
                decided_by: null
                decided_at: null
                result: {}
                error: null
                api_key: null
                response: null
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/LiveRequest"
      x-audience: public
  /v1/live_requests/{id}/approve:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Approve a live request (dashboard, owner or admin)
      tags:
        - AI apps
      operationId: approveLiveRequest
      description: "Runs the operation now, as the approver, in live mode (its Events carry via: the AI app). Its API answer is returned once, to the approver only, in response (and api_key for create_live_api_key): a live key's token, a webhook endpoint's signing secret. Fails (status failed, error) without changing anything if the precondition no longer holds or the operation is refused."
      parameters:
        - name: X-Requested-With
          in: header
          schema:
            type: string
      responses:
        "200":
          description: "approved: the live key, shown once"
          content:
            application/json:
              example:
                object: live_request
                id: lrq_FpLTMa8X3rjugaNz
                action: create_live_api_key
                title: Create a live API key
                summary: Create a live secret (nc_live_…) API key named “Production server”, full access
                params:
                  kind: secret
                  name: Production server
                  access: full
                  permissions: {}
                precondition: null
                reason: Ready for production
                status: approved
                requested_by:
                  type: ai_agent
                  name: Claude Code
                  grant_id: ogr_TH3UBPgpw5dZYQcm
                approval_url: http://localhost:5173/live-requests/lrq_FpLTMa8X3rjugaNz?account=acc_SHWzbh3KJWUUN6ez
                decided_by:
                  id: usr_4NwxZDGRUAd3xyBn
                  email: user1@example.test
                decided_at: 2026-11-04T10:00:00.000Z
                result:
                  object: api_key
                  id: key_wjS3tDF9X8143ppr
                  api_key_id: key_wjS3tDF9X8143ppr
                error: null
                api_key:
                  object: api_key
                  id: key_wjS3tDF9X8143ppr
                  name: Production server
                  kind: secret
                  mode: live
                  access: full
                  permissions:
                    account: write
                    api_keys: write
                    team: write
                    events: write
                    customers: write
                    billing: write
                    stripe_connections: write
                    logs: write
                    flows: write
                    email_domains: write
                    messages: write
                    webhooks: write
                    cancel_sessions: write
                    reviews: write
                    dashboard: write
                    insights: write
                    integrations: write
                    queries: write
                    disputes: write
                    sandbox: write
                  last4: QbM5
                  allowed_origins: null
                  token: nc_live_EXAMPLE00000000000000000
                  first_used_at: null
                  last_used_at: null
                  revoked_at: null
                  created_at: 2026-11-04T10:00:00.000Z
                response:
                  object: api_key
                  id: key_wjS3tDF9X8143ppr
                  name: Production server
                  kind: secret
                  mode: live
                  access: full
                  permissions:
                    account: write
                    api_keys: write
                    team: write
                    events: write
                    customers: write
                    billing: write
                    stripe_connections: write
                    logs: write
                    flows: write
                    email_domains: write
                    messages: write
                    webhooks: write
                    cancel_sessions: write
                    reviews: write
                    dashboard: write
                    insights: write
                    integrations: write
                    queries: write
                    disputes: write
                    sandbox: write
                  last4: QbM5
                  allowed_origins: null
                  token: nc_live_EXAMPLE00000000000000000
                  first_used_at: null
                  last_used_at: null
                  revoked_at: null
                  created_at: 2026-11-04T10:00:00.000Z
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/LiveRequest"
        "403":
          description: a member (read only)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: already decided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/live_requests/{id}/reject:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Reject a live request (dashboard, owner or admin)
      tags:
        - AI apps
      operationId: rejectLiveRequest
      parameters:
        - name: X-Requested-With
          in: header
          schema:
            type: string
      responses:
        "200":
          description: "rejected: nothing runs"
          content:
            application/json:
              example:
                object: live_request
                id: lrq_qjEB7xxu8ZAoUT4y
                action: publish_flow_live
                title: Publish a flow to live mode
                summary: Publish the current draft of Cancel flow to live mode (customers in live mode get it)
                params:
                  flow_id: flw_pr839pHikX4wkNpw
                  flow_kind: cancel
                  flow_name: Cancel flow
                precondition:
                  kind: draft_digest
                  object: flow
                  id: flw_pr839pHikX4wkNpw
                  expected: null
                  status: null
                  description: The flow draft must be the one the app asked to publish.
                reason: Ready for production
                status: rejected
                requested_by:
                  type: ai_agent
                  name: Claude Code
                  grant_id: ogr_cpYehACptMsaMRM6
                approval_url: http://localhost:5173/live-requests/lrq_qjEB7xxu8ZAoUT4y?account=acc_E8fCBPuUNqCUVdoq
                decided_by:
                  id: usr_GPLLsThB1vdVkV4j
                  email: user1@example.test
                decided_at: 2026-11-04T10:00:00.000Z
                result: {}
                error: null
                api_key: null
                response: null
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/LiveRequest"
      x-audience: public
  /v1/logos/{domain}:
    parameters:
      - name: domain
        in: path
        schema:
          type: string
        example: acme.com
        description: A customer's company_domain.
        required: true
      - name: account
        in: query
        required: false
        schema:
          type: string
        example: acc_4fJ8sK2pQ9wLm3Xz
        description: "Dashboard session only: the account, in place of the NoChurn-Account header that an <img> cannot send."
    get:
      summary: A customer's company logo
      tags:
        - Customers
      operationId: getCompanyLogo
      description: "The favicon of a company domain, for the customer's avatar: an image (PNG most often; never SVG), Cache-Control: private, max-age=86400. Only a domain that is the company_domain of a customer of the account answers (404 otherwise: never an open proxy). NoChurn fetches it from Google's favicon service and caches it 30 days: Google never sees who looks at it. 404 with an empty body when the domain has no logo: show your default avatar. A session matches customers of both modes; an API key, those of its mode. Resource customers (read)."
      responses:
        "200":
          description: the logo
          content:
            image/png:
              schema:
                type: string
                format: binary
        "404":
          description: no logo (empty body), or no customer of the account has this company domain (JSON error)
      x-audience: public
  /v1/logs:
    get:
      summary: List the unified log, newest first
      tags:
        - Logs
      operationId: listLogs
      description: "Stripe webhooks, your API calls, NoChurn decisions (and later the widget, outgoing webhooks and support tools), kept 90 days. Live view: poll with after=<poll_cursor> every few seconds."
      parameters:
        - name: source
          in: query
          required: false
          schema:
            type: string
          example: billing,api
          description: "Comma-separated: stripe, api, nochurn, widget, webhook, chargebee, recurly, maxio, zuora, paddle, lemon_squeezy, polar, braintree, mollie, gocardless, razorpay, app_store, google_play, custom, intercom, crisp, zendesk, hubspot, helpscout, gorgias; billing stands for every billing tool (stripe, chargebee, recurly, maxio, zuora, paddle, lemon_squeezy, polar, braintree, mollie, gocardless, razorpay, app_store, google_play, custom)."
        - name: errors
          in: query
          required: false
          schema:
            type: boolean
          description: Errors only.
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: A customer (cst_, cus_, your id, email), an id (log_, nevt_, iwh_) or text.
        - name: customer
          in: query
          required: false
          schema:
            type: string
          description: cst_…, cus_… or your id.
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Default: 24 hours before until. The window is 31 days at most."
        - name: until
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
          description: Older lines.
        - name: after
          in: query
          required: false
          schema:
            type: string
          description: Only newer lines (polling).
      responses:
        "200":
          description: "live polling: nothing new keeps the cursor"
          content:
            application/json:
              example:
                object: list
                data:
                  - object: log_entry
                    id: log_FtFYHxFSjx7e96PG
                    livemode: false
                    occurred_at: 2026-11-04T09:59:00.000Z
                    source: api
                    type: POST /v1/customers
                    customer: null
                    summary: null
                    status: "500"
                    error: true
                    why: null
                    ref: null
                    request_id: null
                  - object: log_entry
                    id: log_6DtqCEhQ718CVQtU
                    livemode: false
                    occurred_at: 2026-11-04T09:58:00.000Z
                    source: stripe
                    type: invoice.payment_failed
                    customer:
                      id: cst_TbLNas4T3Y6wWvgN
                      name: null
                      email: customer1@acme-corp.dev
                    summary: $49.00 · insufficient_funds
                    status: received
                    error: false
                    why: null
                    ref: null
                    request_id: null
                has_more: false
                next_cursor: null
                poll_cursor: log_FtFYHxFSjx7e96PG
                retention_days: 90
              schema:
                $ref: "#/components/schemas/LogEntryPage"
        "400":
          description: unknown source
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: restricted key without logs access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/logs/stats:
    get:
      summary: Count of today's lines and of the errors of a window
      tags:
        - Logs
      operationId: getLogStats
      description: "today_count: today in the account's time zone, all lines. errors_count: the error lines of the window and filters passed (same params as GET /v1/logs, 24 hours by default)."
      parameters:
        - name: source
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated sources, or billing (every billing tool), as GET /v1/logs.
        - name: errors
          in: query
          required: false
          schema:
            type: boolean
          description: "Accepted for symmetry with GET /v1/logs: errors_count only counts errors anyway."
        - name: q
          in: query
          required: false
          schema:
            type: string
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: "Default: 24 hours before until. The window is 31 days at most."
        - name: until
          in: query
          required: false
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: errors of one source over 7 days
          content:
            application/json:
              example:
                object: log_stats
                livemode: false
                timezone: Europe/Paris
                today_count: 4
                errors_count: 3
                retention_days: 90
              schema:
                $ref: "#/components/schemas/LogStats"
        "400":
          description: unknown source
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/logs/{id}:
    get:
      summary: Retrieve a line with its payload
      tags:
        - Logs
      operationId: getLog
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: the line, payload filtered
          content:
            application/json:
              example:
                object: log_entry
                id: log_6NzztFYECRW8iXtw
                livemode: false
                occurred_at: 2026-11-04T10:00:00.000Z
                source: nochurn
                type: x.y
                customer: null
                summary: null
                status: ok
                error: false
                why: null
                ref: null
                request_id: null
                payload:
                  id: in_1
                  client_secret: EXAMPLE000
              schema:
                $ref: "#/components/schemas/LogEntryDetail"
        "404":
          description: line of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/messages/{id}/approve:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Approve an email waiting for review
      tags:
        - Messages
      operationId: approveMessage
      description: It is sent at its slot in the customer's send window, or now if that slot has passed.
      responses:
        "200":
          description: approved
          content:
            application/json:
              example:
                object: message
                id: msg_jybJppHfErmGqPeU
                livemode: false
                channel: email
                customer:
                  id: cst_a48iMM4Qwrzhi2DD
                  name: Lena
                  email: lena@globex.io
                flow_run: null
                template_key: null
                locale: null
                from: acme@mail.nochurn.sh
                from_name: Acme
                reply_to: reply@mail.nochurn.sh
                to: lena@globex.io
                subject: Pay now
                ai_generated: false
                review_state: approved
                review_issues:
                  - Contains a number
                reviewed_at: 2026-11-04T10:00:00Z
                status: queued
                scheduled_for: 2026-11-04T10:00:00Z
                schedule_reasons: []
                sent_at: null
                delivered_at: null
                opened_at: null
                clicked_at: null
                bounced_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                body_text: Pay within 3 days
                body_html: null
              schema:
                $ref: "#/components/schemas/MessageDetail"
        "409":
          description: not waiting for a review
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/messages/{id}/reject:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: "Reject an email: it is never sent"
      tags:
        - Messages
      operationId: rejectMessage
      responses:
        "200":
          description: rejected
          content:
            application/json:
              example:
                object: message
                id: msg_vQ671Sb9uijrVEbZ
                livemode: false
                channel: email
                customer:
                  id: cst_Z3cG1aZVjNGdxEqx
                  name: Lena
                  email: lena@globex.io
                flow_run: null
                template_key: null
                locale: null
                from: acme@mail.nochurn.sh
                from_name: Acme
                reply_to: reply@mail.nochurn.sh
                to: lena@globex.io
                subject: Pay now
                ai_generated: false
                review_state: rejected
                review_issues:
                  - Contains a number
                reviewed_at: 2026-11-04T10:00:00Z
                status: cancelled
                scheduled_for: 2026-11-04T10:00:00Z
                schedule_reasons: []
                sent_at: null
                delivered_at: null
                opened_at: null
                clicked_at: null
                bounced_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                body_text: Pay within 3 days
                body_html: null
              schema:
                $ref: "#/components/schemas/MessageDetail"
      x-audience: public
  /v1/messages/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      summary: Edit an email waiting for review
      tags:
        - Messages
      operationId: updateMessage
      parameters: []
      responses:
        "200":
          description: edited
          content:
            application/json:
              example:
                object: message
                id: msg_RdxeWUzh1bU5G13D
                livemode: false
                channel: email
                customer:
                  id: cst_HP9WD9AFqWFQAYcW
                  name: Lena
                  email: lena@globex.io
                flow_run: null
                template_key: null
                locale: null
                from: acme@mail.nochurn.sh
                from_name: Acme
                reply_to: reply@mail.nochurn.sh
                to: lena@globex.io
                subject: Pay now
                ai_generated: false
                review_state: needs_review
                review_issues:
                  - Contains a number
                reviewed_at: null
                status: queued
                scheduled_for: 2026-11-04T10:00:00Z
                schedule_reasons: []
                sent_at: null
                delivered_at: null
                opened_at: null
                clicked_at: null
                bounced_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                body_text: Please update your card when you can.
                body_html: <p>Please update your card when you can.</p>
              schema:
                $ref: "#/components/schemas/MessageDetail"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subject:
                  type: string
                body_text:
                  type: string
            example:
              body_text: Please update your card when you can.
        required: true
    get:
      summary: One email, with its body
      tags:
        - Messages
      operationId: getMessage
      responses:
        "200":
          description: found
          content:
            application/json:
              example:
                object: message
                id: msg_MVTSkdbxaUw5QLdW
                livemode: false
                channel: email
                customer:
                  id: cst_ELRfdh1vS6cs1nLT
                  name: null
                  email: lena@globex.io
                flow_run: null
                template_key: null
                locale: null
                from: acme@mail.nochurn.sh
                from_name: Acme
                reply_to: reply@mail.nochurn.sh
                to: lena@globex.io
                subject: Your payment failed
                ai_generated: false
                review_state: auto_approved
                review_issues: []
                reviewed_at: null
                status: queued
                scheduled_for: 2026-11-04T10:00:00Z
                schedule_reasons: []
                sent_at: null
                delivered_at: null
                opened_at: null
                clicked_at: null
                bounced_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                body_text: Hi
                body_html: null
              schema:
                $ref: "#/components/schemas/MessageDetail"
      x-audience: public
  /v1/flows/{id}/send_test:
    parameters:
      - name: id
        in: path
        description: flw_… or recovery
        required: true
        schema:
          type: string
    post:
      summary: Send a test email to yourself
      tags:
        - Flows
      operationId: sendFlowTest
      description: "Dashboard only: emails the signed-in user, marked [TEST], with sample data, one email of the saved draft as a customer would get it (your text for the language, else the AI, else NoChurn's template). 20 a day."
      parameters:
        - name: X-Requested-With
          in: header
          required: false
          schema:
            type: string
      responses:
        "201":
          description: sent to the signed-in user
          content:
            application/json:
              example:
                object: flow_test_email
                to: user1@example.test
                subject: "[TEST] Quick reminder about your payment"
                body: |-
                  Hi User,

                  The last payment of €49.00 for Acme didn't go through. You can update your card in one minute here: https://pay.nochurn.sh/example

                  Thanks,
                  Acme
                ai_generated: true
                source: ai
                review:
                  state: auto_approved
                  issues: []
              schema:
                $ref: "#/components/schemas/FlowTestEmail"
        "403":
          description: with an API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - step
              properties:
                step:
                  oneOf:
                    - type: string
                    - type: integer
                  example: email_1
                  description: "The email (as in the email preview: email_1…, card_removed, d30, review_follow_up…); a recovery email also by its index (0, 1, 2)."
                language:
                  type: string
                  example: fr
                  description: "Default: the account's language when the flow writes in it."
            example:
              step: 1
        required: true
  /v1/messages:
    get:
      summary: List emails sent or queued
      tags:
        - Messages
      operationId: listMessages
      parameters:
        - name: customer
          in: query
          required: false
          schema:
            type: string
        - name: status
          in: query
          required: false
          schema:
            type: string
        - name: flow
          in: query
          required: false
          description: flw_… or a kind (recovery…)
          schema:
            type: string
        - name: review_state
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the customer's emails
          content:
            application/json:
              example:
                object: list
                data:
                  - object: message
                    id: msg_7mAdYE7eDfM14gmf
                    livemode: false
                    channel: email
                    customer:
                      id: cst_6VxAaGbvxB6kQ3DU
                      name: null
                      email: lena@globex.io
                    flow_run: null
                    template_key: null
                    locale: null
                    from: acme@mail.nochurn.sh
                    from_name: Acme
                    reply_to: reply@mail.nochurn.sh
                    to: lena@globex.io
                    subject: Your payment failed
                    ai_generated: false
                    review_state: auto_approved
                    review_issues: []
                    reviewed_at: null
                    status: queued
                    scheduled_for: 2026-11-04T10:00:00Z
                    schedule_reasons: []
                    sent_at: null
                    delivered_at: null
                    opened_at: null
                    clicked_at: null
                    bounced_at: null
                    error: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/MessageList"
      x-audience: public
  /v1/email_suppressions:
    get:
      summary: Addresses never emailed again
      tags:
        - Messages
      operationId: listEmailSuppressions
      parameters:
        - name: email
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the list
          content:
            application/json:
              example:
                object: list
                data:
                  - object: email_suppression
                    id: sup_ivHvxoZM6AmGv9EH
                    livemode: false
                    email: x@y.com
                    reason: bounced
                    scope: all
                    message: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/EmailSuppressionList"
      x-audience: public
    post:
      summary: Never email this address again
      tags:
        - Messages
      operationId: createEmailSuppression
      description: Emails still queued to this address are cancelled.
      parameters: []
      responses:
        "201":
          description: added
          content:
            application/json:
              example:
                object: email_suppression
                id: sup_TVZJJc1oa5NXcNFX
                livemode: false
                email: lena@globex.io
                reason: manual
                scope: all
                message: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/EmailSuppression"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
            example:
              email: Lena@globex.io
        required: true
  /v1/email_suppressions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    delete:
      summary: Email this address again
      tags:
        - Messages
      operationId: deleteEmailSuppression
      description: "Unsubscribes, spam complaints and GDPR erasures cannot be removed: only the customer can opt back in."
      responses:
        "204":
          description: removed
        "409":
          description: an unsubscribe
      x-audience: public
  /v1/account/setup/share:
    post:
      summary: Email a setup step to a colleague
      tags:
        - Account
      operationId: shareSetup
      description: « Send to my IT team » / « Email it to a developer ». 10 a day.
      parameters: []
      responses:
        "201":
          description: sent with the DNS records
          content:
            application/json:
              example:
                object: setup_share
                topic: email_domain
                email: it@acme.com
                sent_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SetupShare"
        "400":
          description: unknown topic
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - topic
              properties:
                email:
                  type: string
                topic:
                  type: string
                  enum:
                    - email_domain
                    - api
                    - cancel_modal
                note:
                  type: string
            example:
              email: it@acme.com
              topic: email_domain
        required: true
  /v1/oauth/grants:
    get:
      summary: List the AI apps connected to the account
      tags:
        - AI apps
      operationId: listOauthGrants
      description: "Settings › Connected AI apps: MCP clients authorized through OAuth, with their scopes and last use."
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the connected apps
          content:
            application/json:
              example:
                object: list
                data:
                  - object: oauth_grant
                    id: ogr_ga9szsdYbQPGuhay
                    client:
                      id: mcpc_8NeN1E6YEpZvJ7vh
                      name: Cursor
                      verified: false
                      verified_name: null
                      client_uri: null
                    user:
                      id: usr_DgKVbPfoFB5vggGt
                      email: user1@example.test
                      name: User
                    scopes:
                      - read
                      - configure
                      - sandbox
                    live_read: false
                    live_write: approval_required
                    last_used_at: null
                    revoked_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/OauthGrantList"
      x-audience: public
  /v1/oauth/grants/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      summary: Allow or stop an AI app's live reads
      tags:
        - AI apps
      operationId: updateOauthGrant
      description: "live_read: whether the app may read live data (NoChurn-Mode: live, reads only). Owner or admin, or the member who connected it. Turning it on needs a person in a dashboard session (never an API key or an OAuth token); turning it off works from any of them. Every change writes an Event."
      parameters:
        - name: X-Requested-With
          in: header
          schema:
            type: string
      responses:
        "200":
          description: live reads allowed
          content:
            application/json:
              example:
                object: oauth_grant
                id: ogr_s9Y7BkukioSRcJSa
                client:
                  id: mcpc_JGFoYmVEQ9hSQq9T
                  name: Cursor
                  verified: false
                  verified_name: null
                  client_uri: null
                user:
                  id: usr_Znx8PDYTc71YL9kP
                  email: user2@example.test
                  name: User
                scopes:
                  - read
                  - configure
                  - sandbox
                live_read: true
                live_write: approval_required
                last_used_at: null
                revoked_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/OauthGrant"
        "403":
          description: turning live reads on with an API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - live_read
              properties:
                live_read:
                  type: boolean
            example:
              live_read: true
        required: true
    delete:
      summary: Revoke an AI app's access
      tags:
        - AI apps
      operationId: revokeOauthGrant
      description: Every token of the app stops working at once, and its pending live requests are cancelled (status expired, with the reason in error and a live_request.expired Event). Owner or admin, or the member who connected it.
      parameters:
        - name: X-Requested-With
          in: header
          schema:
            type: string
      responses:
        "200":
          description: revoked
          content:
            application/json:
              example:
                object: oauth_grant
                id: ogr_aAN5xuUxRriy3UW2
                client:
                  id: mcpc_bvMfrNFty8PB8882
                  name: Cursor
                  verified: false
                  verified_name: null
                  client_uri: null
                user:
                  id: usr_8FSUTy7n5pyHtBp9
                  email: user2@example.test
                  name: User
                scopes:
                  - read
                  - configure
                  - sandbox
                live_read: false
                live_write: approval_required
                last_used_at: null
                revoked_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/OauthGrant"
        "403":
          description: a member revoking someone else's app
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: another account's app
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/query_fields:
    get:
      summary: What a query can read
      tags:
        - Data & AI
      operationId: listQueryFields
      description: "NoChurn's query builder: no SQL. Sources and their fields (email as domain only, sensitive attributes excluded), with a description and, for enum-like fields, their values."
      responses:
        "200":
          description: sources and fields
          content:
            application/json:
              example:
                object: list
                has_more: false
                next_cursor: null
                data:
                  - source: customers
                    label: Customers
                    description: The people and companies who pay you, one row per customer.
                    fields:
                      - field: id
                        type: string
                        label: NoChurn id
                        description: NoChurn id of the customer (cst_…).
                      - field: external_id
                        type: string
                        label: Your id
                        description: The customer's id in your app, as you sent it.
                      - field: email_domain
                        type: string
                        label: Email domain
                        description: The part after @ of the customer's email (the address itself is never shown).
                      - field: country
                        type: string
                        label: Country
                        description: Two-letter country code, from billing.
                        values: []
                        values_kind: observed
                      - field: plan
                        type: string
                        label: Plan
                        description: Name of the customer's current plan.
                        values:
                          - value: Pro
                            label: Pro
                        values_kind: observed
                      - field: state
                        type: string
                        label: Billing state
                        description: "Where the customer stands: trialing, active, in recovery, paused, churned, or no subscription."
                        values:
                          - value: none
                            label: No subscription
                          - value: trialing
                            label: Trialing
                          - value: active
                            label: Active
                          - value: in_recovery
                            label: In recovery
                          - value: paused
                            label: Paused
                          - value: churned
                            label: Churned
                        values_kind: closed
                      - field: churn_score
                        type: number
                        label: Churn score
                        description: NoChurn's churn risk score, 0 to 100 (higher is riskier).
                      - field: churn_level
                        type: string
                        label: Churn level
                        description: "The churn score as a level: low, medium or high."
                        values:
                          - value: low
                            label: Low
                          - value: medium
                            label: Medium
                          - value: high
                            label: High
                        values_kind: closed
                      - field: mrr_cents
                        type: number
                        label: MRR (cents)
                        description: Monthly recurring revenue, in cents of the customer's currency.
                      - field: lifetime_value_cents
                        type: number
                        label: Lifetime value (cents)
                        description: Everything the customer paid so far, in cents of their currency.
                      - field: currency
                        type: string
                        label: Currency
                        description: Three-letter currency code, lowercase (eur, usd…).
                        values: []
                        values_kind: observed
                      - field: flagged
                        type: boolean
                        label: Early signal
                        description: An early churn signal is raised on the customer.
                      - field: created_at
                        type: date
                        label: Created
                        description: When the customer was created.
                      - field: churned_at
                        type: date
                        label: Churned
                        description: When the customer churned (empty if they did not).
                      - field: last_seen_at
                        type: date
                        label: Last seen
                        description: Last activity NoChurn knows of in your product.
                      - field: utm_source
                        type: string
                        label: UTM source
                        description: "Where the customer came from (utm_source at signup: google, facebook, newsletter…)."
                        values: []
                        values_kind: observed
                      - field: utm_medium
                        type: string
                        label: UTM medium
                        description: "The marketing channel at signup (utm_medium: cpc, email, social…)."
                        values: []
                        values_kind: observed
                      - field: utm_campaign
                        type: string
                        label: UTM campaign
                        description: The campaign the customer signed up from (utm_campaign).
                        values: []
                        values_kind: observed
                  - source: subscriptions
                    label: Subscriptions
                    description: Each plan a customer subscribes to, with its status and amount.
                    fields:
                      - field: id
                        type: string
                        label: NoChurn id
                        description: NoChurn's id of the row.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: status
                        type: string
                        label: Status
                        description: "The billing status: active, trialing, past due, canceled…"
                        values:
                          - value: incomplete
                            label: Incomplete
                          - value: incomplete_expired
                            label: Incomplete (expired)
                          - value: trialing
                            label: Trialing
                          - value: active
                            label: Active
                          - value: past_due
                            label: Past due
                          - value: canceled
                            label: Canceled
                          - value: unpaid
                            label: Unpaid
                          - value: paused
                            label: Paused
                        values_kind: closed
                      - field: plan
                        type: string
                        label: Plan
                        description: Name of the plan.
                        values: []
                        values_kind: observed
                      - field: interval
                        type: string
                        label: Interval
                        description: How often it bills (month, year…).
                        values: []
                        values_kind: observed
                      - field: amount_cents
                        type: number
                        label: Amount (cents)
                        description: Price per interval, in cents of its currency.
                      - field: currency
                        type: string
                        label: Currency
                        description: Three-letter currency code, lowercase (eur, usd…).
                        values: []
                        values_kind: observed
                      - field: cancel_at_period_end
                        type: boolean
                        label: Cancels at period end
                        description: Canceled, but runs until the end of the paid period.
                      - field: started_at
                        type: date
                        label: Started
                        description: When the subscription started.
                      - field: canceled_at
                        type: date
                        label: Canceled
                        description: When it was canceled.
                      - field: ended_at
                        type: date
                        label: Ended
                        description: When it actually stopped.
                      - field: created_at
                        type: date
                        label: Created
                        description: When it was created.
                  - source: invoices
                    label: Invoices
                    description: Bills sent to customers, with what was due, what was paid and the last decline code.
                    fields:
                      - field: id
                        type: string
                        label: NoChurn id
                        description: NoChurn's id of the row.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: status
                        type: string
                        label: Status
                        description: draft, open (not paid yet), paid, uncollectible or void.
                        values:
                          - value: draft
                            label: Draft
                          - value: open
                            label: Open
                          - value: paid
                            label: Paid
                          - value: uncollectible
                            label: Uncollectible
                          - value: void
                            label: Void
                        values_kind: closed
                      - field: amount_due_cents
                        type: number
                        label: Amount due (cents)
                        description: What the customer owes, in cents.
                      - field: amount_paid_cents
                        type: number
                        label: Amount paid (cents)
                        description: What was paid, in cents.
                      - field: currency
                        type: string
                        label: Currency
                        description: Three-letter currency code, lowercase (eur, usd…).
                        values: []
                        values_kind: observed
                      - field: decline_code
                        type: string
                        label: Decline code
                        description: Why the last payment of the invoice was refused (insufficient_funds…).
                        values: []
                        values_kind: observed
                      - field: attempt_count
                        type: number
                        label: Attempts
                        description: How many times payment was attempted.
                      - field: created_at
                        type: date
                        label: Created
                        description: When the invoice was created.
                  - source: payments
                    label: Payments
                    description: Each payment attempt (a charge), succeeded or failed, with its decline code.
                    fields:
                      - field: id
                        type: string
                        label: NoChurn id
                        description: NoChurn's id of the row.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: status
                        type: string
                        label: Status
                        description: succeeded, failed or pending.
                        values:
                          - value: succeeded
                            label: Succeeded
                          - value: failed
                            label: Failed
                          - value: pending
                            label: Pending
                        values_kind: closed
                      - field: amount_cents
                        type: number
                        label: Amount (cents)
                        description: Amount of the attempt, in cents.
                      - field: amount_refunded_cents
                        type: number
                        label: Refunded (cents)
                        description: Amount refunded, in cents.
                      - field: currency
                        type: string
                        label: Currency
                        description: Three-letter currency code, lowercase (eur, usd…).
                        values: []
                        values_kind: observed
                      - field: decline_code
                        type: string
                        label: Decline code
                        description: Why the bank or the payment provider refused this attempt.
                        values: []
                        values_kind: observed
                      - field: occurred_at
                        type: date
                        label: Date
                        description: When the payment was attempted.
                  - source: cancel_sessions
                    label: Cancel sessions
                    description: Each time a customer opened the cancel flow, with the reason they chose and what happened.
                    fields:
                      - field: id
                        type: string
                        label: Session
                        description: NoChurn id of the cancel session.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: reason
                        type: string
                        label: Reason
                        description: The cancel reason the customer chose (its key in your cancel flow).
                        values: []
                        values_kind: observed
                      - field: outcome
                        type: string
                        label: Outcome
                        description: saved (they stayed), cancelled, or abandoned (closed without deciding).
                        values:
                          - value: saved
                            label: Saved
                          - value: cancelled
                            label: Cancelled
                          - value: abandoned
                            label: Abandoned
                        values_kind: closed
                      - field: offer_accepted
                        type: boolean
                        label: Offer accepted
                        description: The customer took the offer.
                      - field: source
                        type: string
                        label: Source
                        description: "Where the cancel flow was opened: widget, hosted page or API."
                        values:
                          - value: widget
                            label: Widget
                          - value: hosted
                            label: Hosted
                          - value: api
                            label: API
                        values_kind: closed
                      - field: saved_mrr_cents
                        type: number
                        label: MRR saved (cents)
                        description: MRR kept when the customer stayed, in cents.
                      - field: currency
                        type: string
                        label: Currency
                        description: Currency of the subscription (or of the customer).
                        values: []
                        values_kind: observed
                      - field: created_at
                        type: date
                        label: Date
                        description: When the cancel flow was opened.
                  - source: flow_runs
                    label: Flow runs
                    description: Each time a NoChurn flow (prevention, recovery, cancel, win-back) ran for a customer, and its outcome.
                    fields:
                      - field: id
                        type: string
                        label: Run
                        description: NoChurn id of the run.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: flow
                        type: string
                        label: Flow
                        description: "Which flow: prevention, recovery, cancel or win-back."
                        values:
                          - value: prevention
                            label: Prevention
                          - value: recovery
                            label: Recovery
                          - value: cancel
                            label: Cancel
                          - value: winback
                            label: Win-back
                        values_kind: closed
                      - field: state
                        type: string
                        label: State
                        description: active (still running), succeeded, failed or stopped.
                        values:
                          - value: active
                            label: Active
                          - value: succeeded
                            label: Succeeded
                          - value: failed
                            label: Failed
                          - value: stopped
                            label: Stopped
                        values_kind: closed
                      - field: outcome
                        type: string
                        label: Outcome
                        description: "What the run achieved: prevented, recovered, saved, won back, lost…"
                        values:
                          - value: prevented
                            label: Prevented
                          - value: recovered
                            label: Recovered
                          - value: saved
                            label: Saved
                          - value: won_back
                            label: Won back
                          - value: lost
                            label: Lost
                          - value: cancelled
                            label: Cancelled
                          - value: abandoned
                            label: Abandoned
                        values_kind: closed
                      - field: confirmation
                        type: string
                        label: Confirmation
                        description: "Whether the outcome held for 30 days: pending, confirmed or reverted."
                        values:
                          - value: pending
                            label: Pending
                          - value: confirmed
                            label: Confirmed
                          - value: reverted
                            label: Reverted
                        values_kind: closed
                      - field: holdout
                        type: boolean
                        label: Holdout
                        description: In the control group (NoChurn did nothing, to measure the lift).
                      - field: outcome_cents
                        type: number
                        label: Amount (cents)
                        description: Amount recovered, in cents.
                      - field: outcome_mrr_cents
                        type: number
                        label: MRR (cents)
                        description: MRR kept or won back, in cents.
                      - field: currency
                        type: string
                        label: Currency
                        description: Three-letter currency code, lowercase (eur, usd…).
                        values: []
                        values_kind: observed
                      - field: started_at
                        type: date
                        label: Started
                        description: When the run started.
                      - field: outcome_at
                        type: date
                        label: Outcome date
                        description: When the outcome was reached.
                  - source: usage_events
                    label: Usage events
                    description: What customers do in your product, as sent to NoChurn (events and page views).
                    fields:
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: name
                        type: string
                        label: Event
                        description: Name of the event (page_view, or what you send with track).
                        values: []
                        values_kind: observed
                      - field: occurred_at
                        type: date
                        label: Date
                        description: When it happened.
                  - source: messages
                    label: Emails
                    description: The emails NoChurn sent to customers, and whether they were opened or clicked.
                    fields:
                      - field: id
                        type: string
                        label: Email
                        description: NoChurn id of the email.
                      - field: customer
                        type: string
                        label: Customer
                        description: NoChurn id of the customer (cst_…).
                      - field: template
                        type: string
                        label: Template
                        description: Which email of the flow it was.
                        values: []
                        values_kind: observed
                      - field: status
                        type: string
                        label: Status
                        description: queued, sent, delivered, bounced, failed…
                        values:
                          - value: queued
                            label: Queued
                          - value: sending
                            label: Sending
                          - value: sent
                            label: Sent
                          - value: delivered
                            label: Delivered
                          - value: bounced
                            label: Bounced
                          - value: complained
                            label: Complained
                          - value: failed
                            label: Failed
                          - value: suppressed
                            label: Suppressed
                          - value: cancelled
                            label: Cancelled
                        values_kind: closed
                      - field: opened
                        type: boolean
                        label: Opened
                        description: The customer opened it.
                      - field: clicked
                        type: boolean
                        label: Clicked
                        description: The customer clicked a link in it.
                      - field: ai_generated
                        type: boolean
                        label: Written by AI
                        description: The text was written by NoChurn's AI.
                      - field: sent_at
                        type: date
                        label: Sent
                        description: When it was sent.
              schema:
                $ref: "#/components/schemas/QuerySourceList"
      x-audience: public
  /v1/queries/run:
    post:
      summary: Run a query
      tags:
        - Data & AI
      operationId: runQuery
      description: Read only, 5 s at most, 1,000 rows, 60 runs an hour. Each run is recorded.
      parameters: []
      responses:
        "200":
          description: columns and rows
          content:
            application/json:
              example:
                object: query_run
                id: qrn_QWkMX2B2yQAhTU1K
                status: succeeded
                spec:
                  source: customers
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                  fields: []
                  filters: []
                  period: null
                  sort: null
                  limit: 100
                columns:
                  - plan
                  - count
                rows:
                  - - Pro
                    - 1
                row_count: 1
                duration_ms: 42
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/QueryRun"
        "400":
          description: unknown field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                spec:
                  $ref: "#/components/schemas/QuerySpec"
                saved_query:
                  type: string
            example:
              spec:
                source: customers
                metrics:
                  - fn: count
                dimensions:
                  - field: plan
        required: true
  /v1/queries/translate:
    post:
      summary: Ask anything
      tags:
        - Data & AI
      operationId: translateQuery
      description: The AI turns the question into a query spec from the catalogue only (never rows); the same validator applies.
      parameters: []
      responses:
        "200":
          description: a spec to run
          content:
            application/json:
              example:
                object: query_translation
                question: How many customers?
                spec:
                  source: customers
                  metrics:
                    - fn: count
                  dimensions: []
                  fields: []
                  filters: []
                  period: null
                  sort: null
                  limit: 100
                explanation: Count.
              schema:
                $ref: "#/components/schemas/QueryTranslation"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - question
              properties:
                question:
                  type: string
            example:
              question: How many customers?
        required: true
  /v1/saved_queries:
    post:
      summary: Save a query
      tags:
        - Data & AI
      operationId: createSavedQuery
      parameters: []
      responses:
        "201":
          description: saved
          content:
            application/json:
              example:
                object: saved_query
                id: qry_b6R81JjTFD5ZJFpL
                name: Customers by plan
                question: null
                spec:
                  source: customers
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                  fields: []
                  filters: []
                  period: null
                  sort: null
                  limit: 100
                view: chart
                chart: {}
                created_at: 2026-11-04T10:00:00Z
                schedule: null
              schema:
                $ref: "#/components/schemas/SavedQuery"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - spec
              properties:
                name:
                  type: string
                question:
                  type: string
                spec:
                  $ref: "#/components/schemas/QuerySpec"
                view:
                  type: string
                  enum:
                    - table
                    - chart
                chart:
                  type: object
            example:
              name: Customers by plan
              spec:
                source: customers
                metrics:
                  - fn: count
                dimensions:
                  - field: plan
              view: chart
        required: true
    get:
      summary: Saved queries
      tags:
        - Data & AI
      operationId: listSavedQueries
      responses:
        "200":
          description: newest first
          content:
            application/json:
              example:
                object: list
                data: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/SavedQueryList"
      x-audience: public
  /v1/saved_queries/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Rename or change a saved query
      tags:
        - Data & AI
      operationId: updateSavedQuery
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: saved_query
                id: qry_gpKxkTzgkCTFJzo6
                name: Renamed
                question: null
                spec:
                  sort: null
                  limit: 100
                  fields: []
                  period: null
                  source: customers
                  filters: []
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                view: table
                chart: {}
                created_at: 2026-11-04T10:00:00Z
                schedule: null
              schema:
                $ref: "#/components/schemas/SavedQuery"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                question:
                  type: string
                spec:
                  $ref: "#/components/schemas/QuerySpec"
                view:
                  type: string
                  enum:
                    - table
                    - chart
                chart:
                  type: object
            example:
              name: Renamed
        required: true
    delete:
      summary: Delete a saved query (and its schedule)
      tags:
        - Data & AI
      operationId: deleteSavedQuery
      responses:
        "200":
          description: deleted
          content:
            application/json:
              example:
                object: saved_query
                id: qry_A7F5EsvVczn9SwwU
                name: Q
                question: null
                spec:
                  sort: null
                  limit: 100
                  fields: []
                  period: null
                  source: customers
                  filters: []
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                view: table
                chart: {}
                created_at: 2026-11-04T10:00:00Z
                schedule: null
                deleted: true
              schema:
                type: object
                required:
                  - id
                  - deleted
                properties:
                  id:
                    type: string
                  deleted:
                    type: boolean
                    enum:
                      - true
      x-audience: public
  /v1/saved_queries/{id}/schedule:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Schedule a report
      tags:
        - Data & AI
      operationId: scheduleSavedQuery
      parameters: []
      responses:
        "200":
          description: scheduled
          content:
            application/json:
              example:
                object: saved_query
                id: qry_tDhxTamfnwgkYVK4
                name: Q
                question: null
                spec:
                  sort: null
                  limit: 100
                  fields: []
                  period: null
                  source: customers
                  filters: []
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                view: table
                chart: {}
                created_at: 2026-11-04T10:00:00Z
                schedule:
                  cadence: weekly
                  day: thursday
                  time: 11:00
                  timezone: Europe/Paris
                  destination: email
                  target: null
                  next_run_at: 2026-11-05T10:00:00Z
                  last_run: null
              schema:
                $ref: "#/components/schemas/SavedQuery"
        "400":
          description: reports only go to the account's owners and admins
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - cadence
                - destination
              properties:
                cadence:
                  type: string
                  enum:
                    - daily
                    - weekly
                    - monthly
                day:
                  oneOf:
                    - type: string
                      enum:
                        - monday
                        - tuesday
                        - wednesday
                        - thursday
                        - friday
                        - saturday
                        - sunday
                    - type: integer
                      minimum: 1
                      maximum: 28
                  description: "weekly: monday…sunday (monday by default); monthly: 1–28 (1 by default); daily: leave out."
                destination:
                  type: string
                  enum:
                    - email
                    - slack
                target:
                  type: string
                  description: "Email address (default: the account's admins)."
            example:
              cadence: weekly
              day: thursday
              destination: email
        required: true
    delete:
      summary: Stop a report
      tags:
        - Data & AI
      operationId: unscheduleSavedQuery
      responses:
        "200":
          description: no schedule
          content:
            application/json:
              example:
                object: saved_query
                id: qry_1qvCv4aVywRRALhc
                name: Q
                question: null
                spec:
                  sort: null
                  limit: 100
                  fields: []
                  period: null
                  source: customers
                  filters: []
                  metrics:
                    - fn: count
                  dimensions:
                    - field: plan
                view: table
                chart: {}
                created_at: 2026-11-04T10:00:00Z
                schedule: null
              schema:
                $ref: "#/components/schemas/SavedQuery"
      x-audience: public
  /v1/replies:
    get:
      summary: Replies to NoChurn emails (To do)
      tags:
        - Messages
      operationId: listReplies
      description: "Sorted by the AI (or keyword rules). `text` is the customer's: display it as text."
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum:
              - open
              - handled
              - ignored
        - name: category
          in: query
          required: false
          schema:
            type: string
            enum:
              - wants_to_return
              - question
              - not_interested
              - unsubscribe
              - out_of_office
              - other
        - name: customer
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: newest first, with the count still open
          content:
            application/json:
              example:
                object: list
                data:
                  - object: message_reply
                    id: rpl_xW5HbqtzKvVVfCqt
                    message: msg_LS2QZwJVaaT8F3Ah
                    customer: cst_Fm54HKouWUM3Uyyq
                    customer_email: lena@globex.io
                    customer_name: Lena
                    flow_run: null
                    flow: null
                    from: lena@globex.io
                    subject: "Re: We miss you"
                    text: Do you have a cheaper plan?
                    category: question
                    summary: null
                    needs_human: true
                    status: open
                    handled_at: null
                    answer_message: null
                    received_at: 2026-11-04T10:00:00Z
                    livemode: false
                has_more: false
                next_cursor: null
                open_count: 1
              schema:
                $ref: "#/components/schemas/MessageReplyPage"
      x-audience: public
  /v1/replies/{id}/ignore:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Ignore a reply
      tags:
        - Messages
      operationId: ignoreReply
      responses:
        "200":
          description: closed
          content:
            application/json:
              example:
                object: message_reply
                id: rpl_5wxJp3DCg4vPhrHg
                message: msg_vFR7ofzoUzQuc5aW
                customer: cst_dEdhnVf8D1pgPcDU
                customer_email: lena@globex.io
                customer_name: Lena
                flow_run: null
                flow: null
                from: lena@globex.io
                subject: "Re: We miss you"
                text: Do you have a cheaper plan?
                category: question
                summary: null
                needs_human: true
                status: ignored
                handled_at: 2026-11-04T10:00:00Z
                answer_message: null
                received_at: 2026-11-04T10:00:00Z
                livemode: false
              schema:
                $ref: "#/components/schemas/MessageReply"
      x-audience: public
  /v1/replies/{id}/handle:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Mark a reply handled
      tags:
        - Messages
      operationId: handleReply
      responses:
        "200":
          description: closed
          content:
            application/json:
              example:
                object: message_reply
                id: rpl_J7ZecZZmYhhFcKHF
                message: msg_MwzQ5iciTQXVwc3Q
                customer: cst_SyJ5a4KUH3XNYBMZ
                customer_email: lena@globex.io
                customer_name: Lena
                flow_run: null
                flow: null
                from: lena@globex.io
                subject: "Re: We miss you"
                text: Do you have a cheaper plan?
                category: question
                summary: null
                needs_human: true
                status: handled
                handled_at: 2026-11-04T10:00:00Z
                answer_message: null
                received_at: 2026-11-04T10:00:00Z
                livemode: false
              schema:
                $ref: "#/components/schemas/MessageReply"
      x-audience: public
  /v1/messages/{id}/reply:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Answer the customer
      tags:
        - Messages
      operationId: replyToMessage
      description: 'Sent now (it answers the customer: no send window), from the same sender, "Re:" the original subject. The open replies to this email are marked handled.'
      parameters: []
      responses:
        "201":
          description: queued to send now
          content:
            application/json:
              example:
                object: message
                id: msg_kevJoAGQTaFsNwmx
                livemode: false
                channel: email
                customer:
                  id: cst_dsRbczhJGFgyjPTc
                  name: Lena
                  email: lena@globex.io
                flow_run: null
                template_key: reply.human
                locale: null
                from: account1@mail.nochurn.sh
                from_name: Account 1
                reply_to: reply@mail.nochurn.sh
                to: lena@globex.io
                subject: "Re: We miss you"
                ai_generated: false
                review_state: auto_approved
                review_issues: []
                reviewed_at: null
                status: queued
                scheduled_for: 2026-11-04T10:00:00Z
                schedule_reasons: []
                sent_at: null
                delivered_at: null
                opened_at: null
                clicked_at: null
                bounced_at: null
                error: null
                created_at: 2026-11-04T10:00:00Z
                body_text: "Yes: the Starter plan is 19 €/month."
                body_html: null
              schema:
                $ref: "#/components/schemas/MessageDetail"
        "400":
          description: empty text
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - text
              properties:
                text:
                  type: string
            example:
              text: "Yes: the Starter plan is 19 €/month."
        required: true
  /v1/review_platforms:
    get:
      summary: Your review pages
      tags:
        - Cancel flow
      operationId: listReviewPlatforms
      responses:
        "200":
          description: the list
          content:
            application/json:
              example:
                object: list
                data:
                  - object: review_platform
                    id: rvp_DLEKX7d6fQenDkjn
                    kind: g2
                    url: https://g2.com/acme
                    status: active
                    review_count: null
                    rating: null
                    rules: {}
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/ReviewPlatformList"
      x-audience: public
    post:
      summary: Add a review page
      tags:
        - Cancel flow
      operationId: createReviewPlatform
      description: Happy leavers are asked once, ever, on the platform with the fewest NoChurn requests this quarter. No reward is ever offered.
      parameters: []
      responses:
        "201":
          description: added
          content:
            application/json:
              example:
                object: review_platform
                id: rvp_se7mSPq3Ww8gp51o
                kind: trustpilot
                url: https://trustpilot.com/review/acme.com
                status: active
                review_count: null
                rating: null
                rules:
                  countries:
                    - FR
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/ReviewPlatform"
        "422":
          description: not an https link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - url
              properties:
                kind:
                  type: string
                  enum:
                    - g2
                    - capterra
                    - trustpilot
                    - google
                    - app_store
                    - product_hunt
                url:
                  type: string
                review_count:
                  type: integer
                rating:
                  type: number
                rules:
                  type: object
                  properties:
                    countries:
                      type: array
                      items:
                        type: string
                    plans:
                      type: array
                      items:
                        type: string
            example:
              kind: trustpilot
              url: https://trustpilot.com/review/acme.com
              rules:
                countries:
                  - FR
        required: true
  /v1/review_requests/stats:
    get:
      summary: Review requests this month
      tags:
        - Cancel flow
      operationId: getReviewRequestStats
      responses:
        "200":
          description: counts from what NoChurn did
          content:
            application/json:
              example:
                object: review_request_stats
                period_start: 2026-11-01T00:00:00Z
                cancellations: 0
                requested: 0
                clicked: 0
                follow_ups_sent: 0
                by_platform: {}
              schema:
                $ref: "#/components/schemas/ReviewRequestStats"
      x-audience: public
  /v1/customers/{id}/score:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        description: cst_…, cus_… or your own id.
        required: true
    get:
      summary: Churn score of a customer
      tags:
        - Customers
      operationId: getCustomerScore
      description: Heuristic v1 (0–100) with the factors that explain it, the prevention flag, and the last 30 computations.
      responses:
        "200":
          description: the score
          content:
            application/json:
              example:
                object: churn_score
                customer: cst_hnAz8xVcGi19CcbK
                score: 10
                level: low
                model_version: heuristic-v1
                computed_at: 2026-11-04T10:00:00Z
                factors:
                  - key: base
                    label: Starting point
                    points: 10
                flag: null
                history:
                  - score: 10
                    level: low
                    trigger: manual
                    computed_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/ChurnScore"
      x-audience: public
  /v1/customers/{id}/attributes:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Merge custom attributes
      tags:
        - Customers
      operationId: mergeCustomerAttributes
      description: The type of a key is inferred at its first send and locked. null removes the key. 100 keys per account, snake_case, texts of 255 characters at most.
      parameters: []
      responses:
        "200":
          description: merged
          content:
            application/json:
              example:
                object: customer
                id: cst_kWvFWUJMzhonPp9G
                stripe_customer_id: null
                external_id: u_1
                stripe_account: null
                livemode: false
                email: ana@acme.com
                company_domain: acme.com
                name: null
                locale: null
                country: null
                region: null
                timezone: Europe/Paris
                timezone_source: account
                attributes:
                  plan: pro
                  seats: 12
                  beta: true
                churn_score: null
                churn_level: null
                flag: null
                state: none
                plan: null
                mrr_cents: 0
                lifetime_value_cents: 0
                currency: null
                payment_method: null
                last_seen_at: null
                churned_at: null
                flows_paused_at: null
                signup_at: null
                created_at: 2026-11-04T10:00:00.000Z
                updated_at: 2026-11-04T10:00:00.000Z
                identifiers: []
              schema:
                $ref: "#/components/schemas/Customer"
        "422":
          description: "wrong type for a locked attribute: nothing is saved"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - attributes
              properties:
                attributes:
                  type: object
                  additionalProperties: true
                  example:
                    plan: pro
                    seats: 12
            example:
              attributes:
                plan: pro
                seats: 12
                beta: true
        required: true
  /v1/attribute_definitions:
    get:
      summary: Custom attributes of the account
      tags:
        - Data & AI
      operationId: listAttributeDefinitions
      responses:
        "200":
          description: with their type and coverage
          content:
            application/json:
              example:
                object: list
                has_more: false
                next_cursor: null
                data:
                  - object: attribute_definition
                    id: atd_rtVRQsuCTXLR8ka2
                    key: plan
                    type: string
                    sensitive: false
                    customers_count: 1
                    coverage: 1
                    last_seen_at: 2026-11-04T10:00:00Z
                    created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/AttributeDefinitionList"
      x-audience: public
    post:
      summary: Declare an attribute before its first send
      tags:
        - Data & AI
      operationId: createAttributeDefinition
      description: "Locks the type from the start (same rules as the first send: snake_case key, 100 per account and mode). Declaring it again with the same type answers 200; another type is attribute_type_mismatch."
      parameters: []
      responses:
        "201":
          description: declared
          content:
            application/json:
              example:
                object: attribute_definition
                id: atd_7donEcLpikLuJtpn
                key: seats
                type: number
                sensitive: false
                customers_count: 0
                coverage: 0
                last_seen_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/AttributeDefinition"
        "422":
          description: already declared with another type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - key
                - type
              properties:
                key:
                  type: string
                  example: seats
                type:
                  type: string
                  enum:
                    - string
                    - number
                    - boolean
                    - date
                sensitive:
                  type: boolean
                  description: Never sent to the AI.
            example:
              key: seats
              type: number
        required: true
  /v1/attribute_definitions/{key}:
    parameters:
      - name: key
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Mark an attribute sensitive
      tags:
        - Data & AI
      operationId: updateAttributeDefinition
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: attribute_definition
                id: atd_6EfKZkNvSS7YEkpS
                key: plan
                type: string
                sensitive: true
                customers_count: 1
                coverage: 1
                last_seen_at: 2026-11-04T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/AttributeDefinition"
        "404":
          description: unknown attribute
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - sensitive
              properties:
                sensitive:
                  type: boolean
            example:
              sensitive: true
        required: true
  /v1/imports:
    post:
      summary: Import a CSV of customers and attributes
      tags:
        - Data & AI
      operationId: createImport
      description: Send the file as multipart `file`, or its text as `csv`. Columns are mapped by name (customer, external_id, email, stripe_customer, name, country, locale; anything else → attribute:<snake_case>), override with `mapping`. dry_run returns the mapping and the first rows. A row with an unknown customer and an external_id or email creates it.
      parameters: []
      responses:
        "200":
          description: "dry run: proposed mapping and preview"
          content:
            application/json:
              example:
                object: import_preview
                columns:
                  - name: User ID
                    target: external_id
                  - name: Email
                    target: email
                  - name: Plan
                    target: attribute:plan
                rows_total: 1
                rows:
                  - User ID: u_1
                    Email: ana@example.test
                    Plan: pro
              schema:
                $ref: "#/components/schemas/ImportPreview"
        "202":
          description: queued
          content:
            application/json:
              example:
                object: import
                id: imp_HGApj7P3iZ2chfFT
                status: pending
                filename: c.csv
                mapping:
                  user_id: external_id
                  plan: attribute:plan
                  seats: attribute:seats
                rows_total: 3
                rows_imported: 0
                rows_failed: 0
                errors: []
                livemode: false
                created_at: 2026-11-04T10:00:00Z
                started_at: null
                finished_at: null
              schema:
                $ref: "#/components/schemas/Import"
        "400":
          description: no column identifies the customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                csv:
                  type: string
                file:
                  type: string
                  format: binary
                filename:
                  type: string
                mapping:
                  type: object
                  additionalProperties:
                    type: string
                dry_run:
                  type: boolean
            example:
              csv: |
                User ID,Email,Plan
                u_1,ana@example.test,pro
              dry_run: true
          multipart/form-data:
            schema:
              type: object
              properties:
                csv:
                  type: string
                file:
                  type: string
                  format: binary
                filename:
                  type: string
                mapping:
                  type: object
                  additionalProperties:
                    type: string
                dry_run:
                  type: boolean
        required: true
    get:
      summary: Imports
      tags:
        - Data & AI
      operationId: listImports
      responses:
        "200":
          description: newest first
          content:
            application/json:
              example:
                object: list
                data: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/ImportList"
      x-audience: public
  /v1/imports/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    get:
      summary: An import and its error report
      tags:
        - Data & AI
      operationId: getImport
      responses:
        "200":
          description: the import
          content:
            application/json:
              example:
                object: import
                id: imp_T6rNbRAuJxkNV4gX
                status: pending
                filename: null
                mapping:
                  email: email
                rows_total: 1
                rows_imported: 0
                rows_failed: 0
                errors: []
                livemode: false
                created_at: 2026-11-04T10:00:00Z
                started_at: null
                finished_at: null
              schema:
                $ref: "#/components/schemas/Import"
      x-audience: public
  /v1/segment_fields:
    get:
      summary: Fields a segment can use
      tags:
        - Flows
      operationId: listSegmentFields
      responses:
        "200":
          description: by source (stripe, nochurn, custom)
          content:
            application/json:
              example:
                object: list
                has_more: false
                next_cursor: null
                data:
                  - source: nochurn
                    field: churn_score
                    type: number
                    label: Churn score (0–100)
                    ops:
                      - eq
                      - neq
                      - gt
                      - gte
                      - lt
                      - lte
                      - exists
                      - not_exists
                  - source: nochurn
                    field: churn_level
                    type: string
                    label: Churn level
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: nochurn
                    field: state
                    type: string
                    label: Billing state
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: nochurn
                    field: country
                    type: string
                    label: Country
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: nochurn
                    field: locale
                    type: string
                    label: Language
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: nochurn
                    field: tenure_days
                    type: number
                    label: Days as a customer
                    ops:
                      - eq
                      - neq
                      - gt
                      - gte
                      - lt
                      - lte
                      - exists
                      - not_exists
                  - source: nochurn
                    field: flagged
                    type: boolean
                    label: Early signal (FLAGGED)
                    ops:
                      - eq
                      - exists
                      - not_exists
                  - source: nochurn
                    field: mrr_cents
                    type: number
                    label: MRR (minor unit)
                    ops:
                      - eq
                      - neq
                      - gt
                      - gte
                      - lt
                      - lte
                      - exists
                      - not_exists
                  - source: nochurn
                    field: support_contacts
                    type: string
                    label: Support conversations, last 30 days (0, 1–2, 3+)
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: plan
                    type: string
                    label: Plan
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: interval
                    type: string
                    label: Billing interval
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: currency
                    type: string
                    label: Currency
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: payment_method
                    type: string
                    label: Payment method category
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: card_country
                    type: string
                    label: Card country
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
                  - source: stripe
                    field: card_funding
                    type: string
                    label: Card funding
                    ops:
                      - eq
                      - neq
                      - in
                      - not_in
                      - contains
                      - exists
                      - not_exists
              schema:
                $ref: "#/components/schemas/SegmentFieldList"
      x-audience: public
  /v1/segments:
    parameters:
      - name: flow
        in: query
        required: true
        schema:
          type: string
        description: flw_… or a kind.
    get:
      summary: Segments of a flow
      tags:
        - Flows
      operationId: listSegments
      description: "In match order: the first segment whose conditions match wins; Everyone else is always last."
      responses:
        "200":
          description: with Everyone else
          content:
            application/json:
              example:
                object: list
                has_more: false
                next_cursor: null
                data:
                  - object: segment
                    id: seg_H45K9royPTDptPfa
                    flow: flw_adUax5LQBJ5bnTPF
                    name: Everyone else
                    position: 1000000
                    is_default: true
                    conditions:
                      any: []
                    overrides: {}
                    customers_count: null
                    mrr_cents: null
                    counted_at: null
                    created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SegmentList"
      x-audience: public
    post:
      summary: Create a segment
      tags:
        - Flows
      operationId: createSegment
      parameters: []
      responses:
        "201":
          description: created before Everyone else
          content:
            application/json:
              example:
                object: segment
                id: seg_oAiWGSuQoPvjdK6X
                flow: flw_CE1Qp1CyPvsjbJXE
                name: High risk
                position: 0
                is_default: false
                conditions:
                  any:
                    - all:
                        - source: nochurn
                          field: churn_score
                          op: gte
                          value: 70
                overrides:
                  cancel_mode: immediately
                customers_count: null
                mrr_cents: null
                counted_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Segment"
        "422":
          description: unknown field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                conditions:
                  type: object
                  description: "{ any: [ { all: [ { source, field, op, value } ] } ] }"
                overrides:
                  type: object
                  description: "THEN: patch of the flow config for this segment. A null in send_window means follow the flow (not the account): it is not stored."
            example:
              name: High risk
              conditions:
                any:
                  - all:
                      - source: nochurn
                        field: churn_score
                        op: gte
                        value: 70
              overrides:
                cancel_mode: immediately
        required: true
  /v1/segments/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Edit a segment
      tags:
        - Flows
      operationId: updateSegment
      description: "Overrides follow the flow draft's rules: known sections only; the cancel flow's texts and CSS are normalized. A null in send_window means follow the flow (not the account): it is not stored."
      parameters: []
      responses:
        "200":
          description: "a null in send_window follows the flow: not stored"
          content:
            application/json:
              example:
                object: segment
                id: seg_xZtZrJpMirXY6Z62
                flow: flw_yJRnuy8MJBfpxCpi
                name: VIP
                position: 0
                is_default: false
                conditions:
                  any:
                    - all:
                        - op: gte
                          field: churn_score
                          value: 70
                          source: nochurn
                overrides:
                  cancel_mode: immediately
                customers_count: null
                mrr_cents: null
                counted_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/Segment"
        "400":
          description: unknown override section
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                conditions:
                  type: object
                overrides:
                  type: object
            example:
              overrides:
                cancel_mode: immediately
        required: true
    delete:
      summary: Delete a segment
      tags:
        - Flows
      operationId: deleteSegment
      description: "Its draft A/B tests go with it. A segment whose tests ran keeps their results: it cannot be deleted (409)."
      responses:
        "200":
          description: deleted
          content:
            application/json:
              example:
                object: segment
                id: seg_MRdMzGuyEFdBKekA
                flow: flw_1BGQe19r5oviKRhH
                name: VIP
                position: 0
                is_default: false
                conditions:
                  any:
                    - all:
                        - op: gte
                          field: churn_score
                          value: 70
                          source: nochurn
                overrides: {}
                customers_count: null
                mrr_cents: null
                counted_at: null
                created_at: 2026-11-04T10:00:00Z
                deleted: true
              schema:
                type: object
                required:
                  - id
                  - deleted
                properties:
                  id:
                    type: string
                  deleted:
                    type: boolean
        "409":
          description: it has A/B test results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/segments/preview:
    post:
      summary: Count the customers of conditions
      tags:
        - Flows
      operationId: previewSegment
      parameters: []
      responses:
        "200":
          description: live count and MRR
          content:
            application/json:
              example:
                object: segment_preview
                customers_count: 1
                mrr_cents: 4900
                counted_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SegmentPreview"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - conditions
              properties:
                conditions:
                  type: object
            example:
              conditions:
                any:
                  - all:
                      - source: nochurn
                        field: churn_score
                        op: gte
                        value: 70
        required: true
  /v1/experiments:
    post:
      summary: Draft an A/B test
      tags:
        - Flows
      operationId: createExperiment
      description: 2 to 4 variants (A is the control). The winner is picked automatically when a variant is the best with probability ≥ threshold and every variant has min_sample runs.
      parameters: []
      responses:
        "201":
          description: a draft
          content:
            application/json:
              example:
                object: experiment
                id: abt_kpD34ZFDVT9aMwBJ
                flow: flw_uBtoswcyqSYy5vqi
                segment: null
                name: Discount vs pause
                target: offer
                goal_metric: saved
                status: draft
                auto_pick: true
                threshold: 0.95
                min_sample: 100
                started_at: null
                stopped_at: null
                winner: null
                stats_at: null
                created_at: 2026-11-04T10:00:00Z
                variants:
                  - id: abv_EwrAK2JyfLNW7vMd
                    label: A
                    is_control: true
                    weight: 50
                    overrides: {}
                    exposures: 0
                    conversions: 0
                    conversion_rate: null
                    probability_best: null
                  - id: abv_uBa3iRjgQ3iMXEQj
                    label: B
                    is_control: false
                    weight: 50
                    overrides:
                      cancel_mode: immediately
                    exposures: 0
                    conversions: 0
                    conversion_rate: null
                    probability_best: null
              schema:
                $ref: "#/components/schemas/Experiment"
        "400":
          description: invalid variants (duplicate labels, no single control), unknown override section, or another flow's segment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - flow
                - name
                - target
                - goal_metric
                - variants
              properties:
                flow:
                  type: string
                name:
                  type: string
                target:
                  type: string
                  enum:
                    - offer
                    - text
                    - timing
                    - design
                    - channel
                goal_metric:
                  type: string
                  enum:
                    - saved
                    - card_updated_before_renewal
                    - recovered_within_7d
                    - won_back
                segment:
                  type: string
                auto_pick:
                  type: boolean
                threshold:
                  type: number
                min_sample:
                  type: integer
                variants:
                  type: array
                  items:
                    type: object
                    properties:
                      label:
                        type: string
                      weight:
                        type: integer
                      overrides:
                        type: object
                        description: "A null in send_window means follow the flow (not the account): it is not stored."
                      is_control:
                        type: boolean
            example:
              flow: cancel
              name: Discount vs pause
              target: offer
              goal_metric: saved
              variants:
                - weight: 50
                - weight: 50
                  overrides:
                    cancel_mode: immediately
        required: true
    get:
      summary: A/B tests
      tags:
        - Flows
      operationId: listExperiments
      parameters:
        - name: flow
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: newest first
          content:
            application/json:
              example:
                object: list
                data: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/ExperimentList"
      x-audience: public
  /v1/experiments/{id}/{action}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
      - name: action
        in: path
        schema:
          type: string
          enum:
            - start
            - stop
            - pick
        required: true
    post:
      summary: Start, stop, or pick a winner
      tags:
        - Flows
      operationId: transitionExperiment
      parameters: []
      responses:
        "200":
          description: started
          content:
            application/json:
              example:
                object: experiment
                id: abt_11NKySG6YQfrocfH
                flow: flw_PnbiXGGd1R1skZwB
                segment: null
                name: T
                target: offer
                goal_metric: saved
                status: running
                auto_pick: true
                threshold: 0.95
                min_sample: 100
                started_at: 2026-11-04T10:00:00Z
                stopped_at: null
                winner: null
                stats_at: null
                created_at: 2026-11-04T10:00:00Z
                variants:
                  - id: abv_8bRciRLXrwnVhqgf
                    label: A
                    is_control: false
                    weight: 50
                    overrides: {}
                    exposures: 0
                    conversions: 0
                    conversion_rate: null
                    probability_best: null
                  - id: abv_qgyEn9yU48DBcVcW
                    label: B
                    is_control: false
                    weight: 50
                    overrides: {}
                    exposures: 0
                    conversions: 0
                    conversion_rate: null
                    probability_best: null
              schema:
                $ref: "#/components/schemas/Experiment"
        "409":
          description: only a running or stopped test gets a winner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                variant:
                  type: string
                  description: "pick: abv_… or label."
            example: {}
  /v1/account/send_schedule/flow_overrides:
    get:
      summary: Flows that set their own send window options
      tags:
        - Account
      operationId: listSendScheduleFlowOverrides
      description: "In the current mode: the options the published version of each flow sets itself (instead of following Settings › Send schedule), those its draft sets, and the segments (THEN) and A/B variants of the flow that set them (no mode: listed in both; the variants of a running test with applies false). Only flows with at least one."
      parameters:
        - name: keys[]
          in: query
          required: false
          schema:
            type: array
            items:
              type: string
              enum:
                - audience
                - respect_customer_timezone
                - skip_weekends
                - skip_public_holidays
                - windows
                - quiet_hours
                - days_off
          description: Only these options (all by default).
      responses:
        "200":
          description: a flow that follows the account, but one of its segments and A/B variants do not
          content:
            application/json:
              example:
                object: list
                data:
                  - object: send_schedule_flow_override
                    flow:
                      id: flw_3yDzGfxYA3qXrkwX
                      kind: recovery
                      name: Recovery flow
                    version:
                      id: flv_dS81MgAgS7sx1js4
                      number: 1
                      published_to: live
                    published:
                      quiet_hours:
                        to: 08:00
                        from: 21:00
                        enabled: true
                    draft:
                      quiet_hours:
                        to: 08:00
                        from: 21:00
                        enabled: true
                    segments: []
                    variants: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/SendScheduleFlowOverrideList"
      x-audience: public
  /v1/account/send_schedule/apply_to_flows:
    post:
      summary: Make flows follow Settings › Send schedule
      tags:
        - Account
      operationId: applySendScheduleToFlows
      description: "Sets the options to null (follow the account) in the flows that set them: the published version of the current mode is copied into a new version with only those options changed (other draft changes are never published), and the draft loses them too. The segments (THEN) and A/B variants of those flows lose them in place (removed, so they follow their flow; they have no mode and no versions, so this changes both modes). The variants of a running test are not changed. Owners and admins. Nothing to change: nothing happens."
      parameters: []
      responses:
        "200":
          description: a segment and a draft test's variant follow their flow again; a running test is left alone
          content:
            application/json:
              example:
                object: send_schedule_flows_applied
                livemode: true
                applied: 1
                counts:
                  flows: 1
                  segments: 0
                  variants: 0
                data:
                  - object: send_schedule_flow_applied
                    flow:
                      id: flw_cRisxrrykqgKPHdX
                      kind: recovery
                      name: Recovery flow
                    version:
                      id: flv_taVfx8DNTK3tKcwE
                      number: 2
                      published_to: live
                    published:
                      quiet_hours:
                        to: 08:00
                        from: 21:00
                        enabled: true
                      respect_customer_timezone: true
                    draft:
                      quiet_hours:
                        to: 08:00
                        from: 21:00
                        enabled: true
                      respect_customer_timezone: true
                    segments: []
                    variants: []
              schema:
                $ref: "#/components/schemas/SendScheduleFlowsApplied"
        "400":
          description: unknown option
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: a flow of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                keys:
                  type: array
                  items:
                    type: string
                    enum:
                      - audience
                      - respect_customer_timezone
                      - skip_weekends
                      - skip_public_holidays
                      - windows
                      - quiet_hours
                      - days_off
                  description: All options by default.
                flow_ids:
                  type: array
                  items:
                    type: string
                  description: All flows that override by default. A flow's segments and A/B variants are applied with it.
            example:
              keys:
                - quiet_hours
                - respect_customer_timezone
              flow_ids:
                - flw_cRisxrrykqgKPHdX
  /v1/account/send_schedule:
    get:
      summary: When NoChurn may contact your customers
      tags:
        - Account
      operationId: getSendSchedule
      responses:
        "200":
          description: B2B preset by default
          content:
            application/json:
              example:
                object: send_schedule
                audience: null
                overrides: {}
                effective:
                  audience: b2b
                  respect_customer_timezone: true
                  quiet_hours:
                    enabled: true
                    from: 21:00
                    to: 08:00
                  days_off: []
                  skip_weekends: true
                  skip_public_holidays: true
                  windows:
                    weekday:
                      - - 09:00
                        - 17:00
                    weekend: []
                presets:
                  b2b:
                    respect_customer_timezone: true
                    quiet_hours:
                      enabled: true
                      from: 21:00
                      to: 08:00
                    days_off: []
                    skip_weekends: true
                    skip_public_holidays: true
                    windows:
                      weekday:
                        - - 09:00
                          - 17:00
                      weekend: []
                  b2c:
                    respect_customer_timezone: true
                    quiet_hours:
                      enabled: true
                      from: 21:00
                      to: 08:00
                    days_off: []
                    skip_weekends: false
                    skip_public_holidays: false
                    windows:
                      weekday:
                        - - 11:00
                          - 13:00
                        - - 18:00
                          - 21:00
                      weekend:
                        - - 10:00
                          - 20:00
                suggested_audience: null
              schema:
                $ref: "#/components/schemas/SendSchedule"
      x-audience: public
    patch:
      summary: Change the audience or any option
      tags:
        - Account
      operationId: updateSendSchedule
      description: Each option can be switched on or off; null removes an override (back to the preset).
      parameters: []
      responses:
        "200":
          description: B2C, with Christmas Eve off
          content:
            application/json:
              example:
                object: send_schedule
                audience: b2c
                overrides:
                  days_off:
                    - 2026-12-24
                effective:
                  audience: b2c
                  respect_customer_timezone: true
                  quiet_hours:
                    enabled: true
                    from: 21:00
                    to: 08:00
                  days_off:
                    - 2026-12-24
                  skip_weekends: false
                  skip_public_holidays: false
                  windows:
                    weekday:
                      - - 11:00
                        - 13:00
                      - - 18:00
                        - 21:00
                    weekend:
                      - - 10:00
                        - 20:00
                presets:
                  b2b:
                    respect_customer_timezone: true
                    quiet_hours:
                      enabled: true
                      from: 21:00
                      to: 08:00
                    days_off: []
                    skip_weekends: true
                    skip_public_holidays: true
                    windows:
                      weekday:
                        - - 09:00
                          - 17:00
                      weekend: []
                  b2c:
                    respect_customer_timezone: true
                    quiet_hours:
                      enabled: true
                      from: 21:00
                      to: 08:00
                    days_off: []
                    skip_weekends: false
                    skip_public_holidays: false
                    windows:
                      weekday:
                        - - 11:00
                          - 13:00
                        - - 18:00
                          - 21:00
                      weekend:
                        - - 10:00
                          - 20:00
                suggested_audience: null
              schema:
                $ref: "#/components/schemas/SendSchedule"
        "400":
          description: unknown option
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                audience:
                  type: string
                  enum:
                    - b2b
                    - b2c
                skip_weekends:
                  type: boolean
                  nullable: true
                skip_public_holidays:
                  type: boolean
                  nullable: true
                respect_customer_timezone:
                  type: boolean
                  nullable: true
                windows:
                  type: object
                  nullable: true
                quiet_hours:
                  type: object
                  nullable: true
                days_off:
                  type: array
                  nullable: true
                  items:
                    type: string
                    format: date
            example:
              audience: b2c
              days_off:
                - 2026-12-24
        required: true
  /v1/customers/{id}/next_send_slot:
    get:
      summary: When NoChurn would contact this customer now
      tags:
        - Customers
      operationId: getNextSendSlot
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: flow
          in: query
          required: false
          schema:
            type: string
            enum:
              - prevention
              - recovery
              - cancel
              - winback
          description: Apply this flow's own send window.
      responses:
        "200":
          description: the next slot in the customer's time zone, with the reasons of a delay
          content:
            application/json:
              example:
                object: send_slot
                customer: cst_HzkSj31QvesBjiLa
                at: 2026-11-04T11:00:00+01:00
                timezone: Europe/Berlin
                timezone_source: address
                delayed: false
                reasons: []
                audience: b2b
              schema:
                $ref: "#/components/schemas/SendSlot"
      x-audience: public
  /v1/sending_domains:
    get:
      summary: List sending domains
      tags:
        - Email domain
      operationId: listSendingDomains
      responses:
        "200":
          description: the account's domain
          content:
            application/json:
              example:
                object: list
                data:
                  - object: sending_domain
                    id: sdm_ua6hYT8P6PmrVtzo
                    domain: acme.com
                    sender_name: Acme billing
                    sender_email: billing@acme.com
                    sending_host: nc-utj7.acme.com
                    status: pending
                    provider: fake
                    provisioning_error: null
                    verified_at: null
                    last_checked_at: null
                    existing_dmarc: false
                    fallback_address: acme@mail.nochurn.sh
                    from_address: acme@mail.nochurn.sh
                    dns_records:
                      - type: TXT
                        host: nc-utj7.acme.com
                        value: v=spf1 include:mailgun.org ~all
                        purpose: sending (SPF)
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: CNAME
                        host: pdk1._domainkey.nc-utj7.acme.com
                        value: pdk1._domainkey.0ae1cc2a3899306ae7550445.fake-dkim.mailgun.invalid
                        purpose: signature (DKIM)
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: CNAME
                        host: pdk2._domainkey.nc-utj7.acme.com
                        value: pdk2._domainkey.0ae1cc2a3899306ae7550445.fake-dkim.mailgun.invalid
                        purpose: signature (DKIM)
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: CNAME
                        host: email.nc-utj7.acme.com
                        value: eu.mailgun.org
                        purpose: link tracking
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: MX
                        host: nc-utj7.acme.com
                        value: 10 mxa.eu.mailgun.org
                        purpose: replies, bounces
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: MX
                        host: nc-utj7.acme.com
                        value: 10 mxb.eu.mailgun.org
                        purpose: replies, bounces
                        optional: false
                        status: pending
                        found: null
                        checked_at: null
                      - type: TXT
                        host: _dmarc.acme.com
                        value: v=DMARC1; p=none; rua=mailto:dmarc@acme.com
                        purpose: policy
                        optional: true
                        status: pending
                        found: null
                        checked_at: null
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/SendingDomainList"
      x-audience: public
    post:
      summary: Add your sending domain
      tags:
        - Email domain
      operationId: createSendingDomain
      description: "Emails go out from a random nc-xxxx subdomain, created at Mailgun: your main domain and your team's mailboxes are never touched. The domain starts in `provisioning`: its DNS records (SPF, DKIM, link tracking, MX) arrive within seconds, once Mailgun answered. Until the records are found and Mailgun confirms them, emails come from <account>@mail.nochurn.sh."
      parameters: []
      responses:
        "201":
          description: domain added, records to create
          content:
            application/json:
              example:
                object: sending_domain
                id: sdm_R37t9AFsE55k3v9L
                domain: acme.com
                sender_name: Acme billing
                sender_email: billing@acme.com
                sending_host: nc-769z.acme.com
                status: provisioning
                provider: fake
                provisioning_error: null
                verified_at: null
                last_checked_at: null
                existing_dmarc: false
                fallback_address: acme@mail.nochurn.sh
                from_address: acme@mail.nochurn.sh
                dns_records: []
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SendingDomain"
        "422":
          description: sender outside the domain
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - domain
                - sender_email
              properties:
                domain:
                  type: string
                  example: acme.com
                sender_email:
                  type: string
                  example: billing@acme.com
                sender_name:
                  type: string
                  example: Acme billing
            example:
              domain: acme.com
              sender_email: billing@acme.com
              sender_name: Acme billing
        required: true
  /v1/sending_domains/{id}/verify:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Check the DNS records
      tags:
        - Email domain
      operationId: verifySendingDomain
      responses:
        "200":
          description: every record found
          content:
            application/json:
              example:
                object: sending_domain
                id: sdm_XajGHMam4v9qgRkg
                domain: acme.com
                sender_name: Acme billing
                sender_email: billing@acme.com
                sending_host: nc-4hmm.acme.com
                status: verified
                provider: fake
                provisioning_error: null
                verified_at: 2026-11-04T10:00:00Z
                last_checked_at: 2026-11-04T10:00:00Z
                existing_dmarc: false
                fallback_address: acme@mail.nochurn.sh
                from_address: billing@acme.com
                dns_records:
                  - type: TXT
                    host: nc-4hmm.acme.com
                    value: v=spf1 include:mailgun.org ~all
                    purpose: sending (SPF)
                    optional: false
                    status: verified
                    found: v=spf1 include:mailgun.org ~all
                    checked_at: 2026-11-04T10:00:00Z
                  - type: CNAME
                    host: pdk1._domainkey.nc-4hmm.acme.com
                    value: pdk1._domainkey.6b66132b30f8f0fe6a014a82.fake-dkim.mailgun.invalid
                    purpose: signature (DKIM)
                    optional: false
                    status: verified
                    found: pdk1._domainkey.6b66132b30f8f0fe6a014a82.fake-dkim.mailgun.invalid
                    checked_at: 2026-11-04T10:00:00Z
                  - type: CNAME
                    host: pdk2._domainkey.nc-4hmm.acme.com
                    value: pdk2._domainkey.6b66132b30f8f0fe6a014a82.fake-dkim.mailgun.invalid
                    purpose: signature (DKIM)
                    optional: false
                    status: verified
                    found: pdk2._domainkey.6b66132b30f8f0fe6a014a82.fake-dkim.mailgun.invalid
                    checked_at: 2026-11-04T10:00:00Z
                  - type: CNAME
                    host: email.nc-4hmm.acme.com
                    value: eu.mailgun.org
                    purpose: link tracking
                    optional: false
                    status: verified
                    found: eu.mailgun.org
                    checked_at: 2026-11-04T10:00:00Z
                  - type: MX
                    host: nc-4hmm.acme.com
                    value: 10 mxa.eu.mailgun.org
                    purpose: replies, bounces
                    optional: false
                    status: verified
                    found: 10 mxa.eu.mailgun.org
                    checked_at: 2026-11-04T10:00:00Z
                  - type: MX
                    host: nc-4hmm.acme.com
                    value: 10 mxb.eu.mailgun.org
                    purpose: replies, bounces
                    optional: false
                    status: verified
                    found: 10 mxa.eu.mailgun.org
                    checked_at: 2026-11-04T10:00:00Z
                  - type: TXT
                    host: _dmarc.acme.com
                    value: v=DMARC1; p=none; rua=mailto:dmarc@acme.com
                    purpose: policy
                    optional: true
                    status: verified
                    found: v=DMARC1; p=none; rua=mailto:dmarc@acme.com
                    checked_at: 2026-11-04T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SendingDomain"
        "409":
          description: records still being prepared by Mailgun
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/sending_domains/{id}/regenerate:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: New random subdomain
      tags:
        - Email domain
      operationId: regenerateSendingDomain
      description: "Only while the domain is not verified: the old nc-xxxx is deleted at Mailgun and the new one prepared (status provisioning, new DNS records)."
      responses:
        "200":
          description: a new subdomain, being prepared
          content:
            application/json:
              example:
                object: sending_domain
                id: sdm_M8nVdo71jUpjJkUt
                domain: acme.com
                sender_name: Acme billing
                sender_email: billing@acme.com
                sending_host: nc-aya7.acme.com
                status: provisioning
                provider: fake
                provisioning_error: null
                verified_at: null
                last_checked_at: null
                existing_dmarc: false
                fallback_address: acme@mail.nochurn.sh
                from_address: acme@mail.nochurn.sh
                dns_records: []
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SendingDomain"
        "409":
          description: already verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/sending_domains/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Retrieve the sending domain
      tags:
        - Email domain
      operationId: getSendingDomain
      responses:
        "200":
          description: a domain Mailgun could not prepare, with what to do
          content:
            application/json:
              example:
                object: sending_domain
                id: sdm_UWVmvucdELPGdftg
                domain: acme.com
                sender_name: Acme billing
                sender_email: billing@acme.com
                sending_host: nc-jtpt.acme.com
                status: provisioning
                provider: fake
                provisioning_error:
                  code: email_domain_provisioning_failed
                  message: NoChurn could not prepare the DNS records of this domain with its email provider.
                  fix: Click Regenerate to try with a new subdomain. Until then, emails keep coming from your NoChurn address.
                  docs_url: https://nochurn.sh/docs/errors#email_domain_provisioning_failed
                verified_at: null
                last_checked_at: null
                existing_dmarc: false
                fallback_address: acme@mail.nochurn.sh
                from_address: acme@mail.nochurn.sh
                dns_records: []
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SendingDomain"
      x-audience: public
    patch:
      summary: Change the sender
      tags:
        - Email domain
      operationId: updateSendingDomain
      parameters: []
      responses:
        "200":
          description: sender changed
          content:
            application/json:
              example:
                object: sending_domain
                id: sdm_q2CtBG9Bvps85iEa
                domain: acme.com
                sender_name: Acme billing
                sender_email: team@acme.com
                sending_host: nc-qa62.acme.com
                status: pending
                provider: fake
                provisioning_error: null
                verified_at: null
                last_checked_at: null
                existing_dmarc: false
                fallback_address: acme@mail.nochurn.sh
                from_address: acme@mail.nochurn.sh
                dns_records:
                  - type: TXT
                    host: nc-qa62.acme.com
                    value: v=spf1 include:mailgun.org ~all
                    purpose: sending (SPF)
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: CNAME
                    host: pdk1._domainkey.nc-qa62.acme.com
                    value: pdk1._domainkey.a1b412bf3158ea43cca9af10.fake-dkim.mailgun.invalid
                    purpose: signature (DKIM)
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: CNAME
                    host: pdk2._domainkey.nc-qa62.acme.com
                    value: pdk2._domainkey.a1b412bf3158ea43cca9af10.fake-dkim.mailgun.invalid
                    purpose: signature (DKIM)
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: CNAME
                    host: email.nc-qa62.acme.com
                    value: eu.mailgun.org
                    purpose: link tracking
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: MX
                    host: nc-qa62.acme.com
                    value: 10 mxa.eu.mailgun.org
                    purpose: replies, bounces
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: MX
                    host: nc-qa62.acme.com
                    value: 10 mxb.eu.mailgun.org
                    purpose: replies, bounces
                    optional: false
                    status: pending
                    found: null
                    checked_at: null
                  - type: TXT
                    host: _dmarc.acme.com
                    value: v=DMARC1; p=none; rua=mailto:dmarc@acme.com
                    purpose: policy
                    optional: true
                    status: pending
                    found: null
                    checked_at: null
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/SendingDomain"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sender_email:
                  type: string
                sender_name:
                  type: string
            example:
              sender_email: team@acme.com
        required: true
    delete:
      summary: Remove the domain
      tags:
        - Email domain
      operationId: deleteSendingDomain
      responses:
        "204":
          description: removed
        "404":
          description: another account's domain
      x-audience: public
  /v1/stripe_connections:
    get:
      summary: List connected Stripe accounts (current mode)
      tags:
        - Stripe
      operationId: listStripeConnections
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: connections, tokens never included
          content:
            application/json:
              example:
                object: list
                data:
                  - object: stripe_connection
                    id: scn_3oj4t6Cs5RBTByvp
                    stripe_account_id: acct_A
                    livemode: false
                    status: active
                    smart_retries_mode: stripe
                    internal_id:
                      field: null
                      defaults:
                        - metadata.user_id
                      fields:
                        - metadata.<name>
                    backfill:
                      status: done
                      progress:
                        customers: 3
                      error: null
                      started_at: null
                      finished_at: null
                    history:
                      status: pending
                      since: null
                      progress: {}
                      error: null
                      started_at: null
                      finished_at: null
                    last_webhook_at: 2026-11-04T09:58:00.000Z
                    last_webhook_type: invoice.payment_failed
                    disconnected_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/StripeConnectionList"
      x-audience: public
  /v1/stripe_connections/authorize:
    post:
      summary: Get the Stripe Connect URL to open in the browser
      tags:
        - Stripe
      operationId: authorizeStripe
      responses:
        "200":
          description: Stripe Connect OAuth URL, with a signed state
          content:
            application/json:
              example:
                object: stripe_authorize_url
                url: https://connect.stripe.com/oauth/authorize?client_id=ca_test_dummy&response_type=code&scope=read_write&state=eyJfcmFpbHMiOnsiZGF0YSI6eyJhY2NvdW50X2lkIjoiYWNjX1JpVWpWOUFqNTNYeTh3djQiLCJ1c2VyX2lkIjpudWxsLCJsaXZlbW9kZSI6ZmFsc2UsIm5vbmNlIjoiOGQzZDI1YzA4NDEzNThiNyJ9LCJleHAiOiIyMDI2LTExLTA0VDEwOjMwOjAwLjAwMFoiLCJwdXIiOiJzdHJpcGVfY29ubmVjdCJ9fQ%3D%3D--6ed54c807385687846013962ddefc306141e6dc2
                livemode: false
              schema:
                $ref: "#/components/schemas/StripeAuthorizeUrl"
        "503":
          description: Stripe not configured on this server
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/stripe_connections/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    patch:
      summary: Choose who handles payment retries, or where your user id is read
      tags:
        - Stripe
      operationId: updateStripeConnection
      parameters: []
      responses:
        "200":
          description: updated
          content:
            application/json:
              example:
                object: stripe_connection
                id: scn_4MU3M4xF2KByearr
                stripe_account_id: acct_A
                livemode: false
                status: active
                smart_retries_mode: nochurn
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                  fields:
                    - metadata.<name>
                backfill:
                  status: done
                  progress:
                    customers: 3
                  error: null
                  started_at: null
                  finished_at: null
                history:
                  status: pending
                  since: null
                  progress: {}
                  error: null
                  started_at: null
                  finished_at: null
                last_webhook_at: null
                last_webhook_type: null
                disconnected_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/StripeConnection"
        "422":
          description: invalid mode
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                smart_retries_mode:
                  type: string
                  enum:
                    - stripe
                    - nochurn
                internal_id_field:
                  type: string
                  nullable: true
                  example: metadata.user_id
                  description: Owner / admin. The Stripe customer metadata key holding your app's id (metadata.<key>); null = metadata.<account external_id_metadata_key>.
            example:
              smart_retries_mode: nochurn
        required: true
    delete:
      summary: Disconnect a Stripe account (synced data is kept)
      tags:
        - Stripe
      operationId: disconnectStripe
      responses:
        "200":
          description: revoked
          content:
            application/json:
              example:
                object: stripe_connection
                id: scn_7U2ckwVunr6xCYko
                stripe_account_id: acct_A
                livemode: false
                status: revoked
                smart_retries_mode: stripe
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                  fields:
                    - metadata.<name>
                backfill:
                  status: done
                  progress:
                    customers: 3
                  error: null
                  started_at: null
                  finished_at: null
                history:
                  status: pending
                  since: null
                  progress: {}
                  error: null
                  started_at: null
                  finished_at: null
                last_webhook_at: null
                last_webhook_type: null
                disconnected_at: 2026-11-04T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/StripeConnection"
        "404":
          description: connection of another account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/stripe_connections/{id}/backfill:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
    post:
      summary: Re-run the Stripe import (idempotent)
      tags:
        - Stripe
      operationId: backfillStripe
      responses:
        "202":
          description: import queued
          content:
            application/json:
              example:
                object: stripe_connection
                id: scn_29duu2TEteBrZ5L3
                stripe_account_id: acct_A
                livemode: false
                status: active
                smart_retries_mode: stripe
                internal_id:
                  field: null
                  defaults:
                    - metadata.user_id
                  fields:
                    - metadata.<name>
                backfill:
                  status: pending
                  progress:
                    customers: 3
                  error: null
                  started_at: null
                  finished_at: null
                history:
                  status: pending
                  since: null
                  progress: {}
                  error: null
                  started_at: null
                  finished_at: null
                last_webhook_at: null
                last_webhook_type: null
                disconnected_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/StripeConnection"
        "409":
          description: already running
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/account/setup:
    get:
      summary: "Setup checklist: what is installed and what is missing"
      tags:
        - Account
      operationId: getSetup
      responses:
        "200":
          description: "billing by API: no Stripe import step; an active support tool counts"
          content:
            application/json:
              example:
                object: setup
                livemode: false
                billing_source: stripe
                api_base_url: https://api.nochurn.sh
                complete: false
                steps:
                  - key: billing_connected
                    done: true
                    fix: null
                    skipped: false
                  - key: billing_synced
                    done: true
                    fix: null
                    skipped: false
                  - key: history_imported
                    done: false
                    fix: Wait for the 12-month history import to finish, or re-run the import from Integrations.
                    skipped: false
                  - key: webhook_received
                    done: false
                    fix: Trigger an event in your billing tool (e.g. update a customer) and check it arrives.
                    skipped: false
                  - key: api_key_created
                    done: true
                    fix: null
                    skipped: false
                  - key: first_api_call
                    done: true
                    fix: null
                    skipped: false
                  - key: support_connected
                    done: false
                    fix: Connect your support tool (Intercom, Zendesk, Crisp, HubSpot, Help Scout or Gorgias) from Integrations.
                    skipped: false
                  - key: cancel_modal_detected
                    done: false
                    fix: "Install the cancel modal on your cancel button: NoChurn detects its first load."
                    skipped: false
                  - key: email_domain_verified
                    done: false
                    fix: Add your sending domain and its DNS records, then check them (Settings › Email domain).
                    skipped: false
                  - key: cancel_flow_published
                    done: false
                    fix: Publish your cancel flow (test mode first) from Cancel flow › Settings.
                    skipped: false
                  - key: recovery_retries_configured
                    done: true
                    fix: null
                    skipped: false
                  - key: prevention_signals_enabled
                    done: true
                    fix: null
                    skipped: false
                onboarding:
                  step: billing
                  skipped: []
                  completed_at: null
                counters:
                  customers_imported: 0
                  active_customers: 0
                  mrr: []
                  failed_payments_to_recover:
                    count: 0
                    amounts: []
                  high_risk_customers: 0
                  scored_customers: 0
                  first_api_call_at: 2026-11-04T10:00:00.000Z
                  customers_with_multiple_subscriptions: 0
                  sessions_without_subscription_30d: 0
                history_import:
                  status: pending
                  since: null
                  progress: {}
                  error: null
                  started_at: null
                  finished_at: null
              schema:
                $ref: "#/components/schemas/Setup"
      x-audience: public
  /v1/memberships:
    get:
      summary: Team members of the account
      tags:
        - Team
      operationId: listMemberships
      responses:
        "200":
          description: members with their role and 2FA status
          content:
            application/json:
              example:
                object: list
                data:
                  - object: membership
                    id: mbr_cUfc8mTHAU7Eg2co
                    role: member
                    user:
                      id: usr_Vv4SRgjqTic423gn
                      email: user1@example.test
                      name: User
                      two_factor_enabled: false
                      last_login_at: null
                    created_at: 2026-11-04T10:00:00.000Z
                  - object: membership
                    id: mbr_6TshrrzHbJhPfMdK
                    role: owner
                    user:
                      id: usr_zt28zC2o6Zn1qFiY
                      email: owner@acme.test
                      name: User
                      two_factor_enabled: false
                      last_login_at: 2026-11-04T10:00:00.000Z
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/MembershipList"
      x-audience: public
  /v1/memberships/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
      - name: X-Requested-With
        in: header
        schema:
          type: string
          enum:
            - nochurn-dashboard
    patch:
      summary: Change a member's role
      tags:
        - Team
      operationId: updateMembership
      parameters: []
      responses:
        "200":
          description: role changed
          content:
            application/json:
              example:
                object: membership
                id: mbr_3Sff9nvg44dKN6ME
                role: admin
                user:
                  id: usr_9WtFa8kpPXoZueb9
                  email: user1@example.test
                  name: User
                  two_factor_enabled: false
                  last_login_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Membership"
        "409":
          description: the last owner cannot step down
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum:
                    - owner
                    - admin
                    - member
            example:
              role: admin
        required: true
    delete:
      summary: Remove a member (or leave the account)
      tags:
        - Team
      operationId: removeMembership
      responses:
        "200":
          description: removed
          content:
            application/json:
              example:
                object: membership
                id: mbr_P3Pc9sSuX3tF4giw
                role: admin
                user:
                  id: usr_VHAaeTFR9fDTrsq3
                  email: user1@example.test
                  name: User
                  two_factor_enabled: false
                  last_login_at: null
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Membership"
      x-audience: public
  /v1/invitations:
    parameters:
      - name: X-Requested-With
        in: header
        schema:
          type: string
          enum:
            - nochurn-dashboard
    post:
      summary: Invite someone (returns the link to send, once)
      tags:
        - Team
      operationId: createInvitation
      parameters: []
      responses:
        "201":
          description: invitation with its link
          content:
            application/json:
              example:
                object: invitation
                id: inv_myc1pyd2keYC2jWD
                email: new@acme.test
                role: admin
                status: pending
                url: http://localhost:5173/invite/Au8qSVmR1hhU9hjFeJsbn4JVkfrKT4Dj
                invited_by: owner@acme.test
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Invitation"
        "409":
          description: already a member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                role:
                  type: string
                  enum:
                    - owner
                    - admin
                    - member
            example:
              email: New@Acme.test
              role: admin
        required: true
    get:
      summary: Pending invitations
      tags:
        - Team
      operationId: listInvitations
      responses:
        "200":
          description: pending invitations (links are never shown again)
          content:
            application/json:
              example:
                object: list
                data:
                  - object: invitation
                    id: inv_2ZfRhgxHtsGee7h6
                    email: a@acme.test
                    role: member
                    status: pending
                    url: null
                    invited_by: owner@acme.test
                    expires_at: 2026-11-11T10:00:00.000Z
                    created_at: 2026-11-04T10:00:00.000Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/InvitationList"
      x-audience: public
  /v1/invitations/{id}:
    parameters:
      - name: id
        in: path
        schema:
          type: string
        required: true
      - name: X-Requested-With
        in: header
        schema:
          type: string
          enum:
            - nochurn-dashboard
    delete:
      summary: Cancel a pending invitation
      tags:
        - Team
      operationId: revokeInvitation
      responses:
        "200":
          description: cancelled
          content:
            application/json:
              example:
                object: invitation
                id: inv_EBdyWjdifwMigLWC
                email: bye@acme.test
                role: member
                status: revoked
                url: null
                invited_by: owner@acme.test
                expires_at: 2026-11-11T10:00:00.000Z
                created_at: 2026-11-04T10:00:00.000Z
              schema:
                $ref: "#/components/schemas/Invitation"
        "409":
          description: already used or cancelled
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
  /v1/webhook_endpoints:
    get:
      summary: List webhook endpoints
      tags:
        - Developers
      operationId: listWebhookEndpoints
      responses:
        "200":
          description: the list, never the secrets
          content:
            application/json:
              example:
                object: list
                data:
                  - object: webhook_endpoint
                    id: whk_qPf7RbQXC4oY7SjA
                    livemode: false
                    url: https://hooks.acme.com/x
                    description: null
                    events:
                      - "*"
                    status: enabled
                    secret: null
                    last_status: null
                    last_delivery_at: null
                    failure_streak: 0
                    created_at: 2026-11-04T10:00:00Z
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/WebhookEndpointList"
      x-audience: public
    post:
      summary: Add a webhook endpoint
      tags:
        - Developers
      operationId: createWebhookEndpoint
      description: 'Deliveries are signed: NoChurn-Signature: t=<unix>,v1=<HMAC-SHA256 of "t.body">. The secret is shown once.'
      parameters: []
      responses:
        "201":
          description: with its secret, once
          content:
            application/json:
              example:
                object: webhook_endpoint
                id: whk_PjDej35aHSE8rdvD
                livemode: false
                url: https://hooks.acme.com/nochurn
                description: null
                events:
                  - recovery.recovered
                  - billing.retry_requested
                status: enabled
                secret: whsec_EXAMPLE00000000000000000
                last_status: null
                last_delivery_at: null
                failure_streak: 0
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "422":
          description: not https or an unknown event
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - events
              properties:
                url:
                  type: string
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - recovery.opened
                      - recovery.recovered
                      - recovery.lost
                      - outcome.confirmed
                      - outcome.reverted
                      - billing.retry_requested
                      - billing.cancel_requested
                      - billing.discount_requested
                      - billing.pause_requested
                      - billing.downgrade_requested
                      - customer.scored
                      - customer.flagged
                      - customer.access_restricted
                      - customer.access_restored
                      - cancel_session.started
                      - cancel_session.offer_accepted
                      - cancel_session.completed
                      - winback.reactivated
                      - "*"
                description:
                  type: string
            example:
              url: https://hooks.acme.com/nochurn
              events:
                - recovery.recovered
                - billing.retry_requested
        required: true
  /v1/webhook_endpoints/{id}/test:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Send a signed test event now
      tags:
        - Developers
      operationId: testWebhookEndpoint
      responses:
        "201":
          description: delivered
          content:
            application/json:
              example:
                object: webhook_delivery
                id: whd_7Fx1qjkiZwLiEgRG
                livemode: false
                endpoint: whk_S569GCh7fAHwYDn7
                destination: http
                event: null
                type: recovery.opened
                status: delivered
                attempts: 1
                next_attempt_at: null
                response_status: 200
                response_ms: 42
                error: null
                delivered_at: 2026-11-04T10:00:00Z
                failed_at: null
                replay_of: null
                created_at: 2026-11-04T10:00:00Z
                payload:
                  id: nevt_test_5accf2bcd660
                  data:
                    test: true
                    message: Test webhook from NoChurn
                  type: test
                  object: event
                  created: 1793786400
                  livemode: false
                response_excerpt: null
              schema:
                $ref: "#/components/schemas/WebhookDeliveryDetail"
      x-audience: public
  /v1/webhook_deliveries:
    get:
      summary: Webhooks sent, with their attempts
      tags:
        - Developers
      operationId: listWebhookDeliveries
      parameters:
        - name: status
          in: query
          required: false
          enum:
            - pending
            - retrying
            - delivered
            - failed
          schema:
            type: string
          description: ":

            \ * `pending`\ 

            \ * `retrying`\ 

            \ * `delivered`\ 

            \ * `failed`\ 

            \ "
        - name: endpoint
          in: query
          required: false
          schema:
            type: string
        - name: type
          in: query
          required: false
          schema:
            type: string
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: the list
          content:
            application/json:
              example:
                object: list
                data: []
                has_more: false
                next_cursor: null
              schema:
                $ref: "#/components/schemas/WebhookDeliveryList"
      x-audience: public
  /v1/webhook_endpoints/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    patch:
      summary: Change an endpoint, disable it or rotate its secret
      tags:
        - Developers
      operationId: updateWebhookEndpoint
      parameters: []
      responses:
        "200":
          description: secret rotated
          content:
            application/json:
              example:
                object: webhook_endpoint
                id: whk_6Z1istZvLLDV7b7p
                livemode: false
                url: https://hooks.acme.com/x
                description: null
                events:
                  - "*"
                status: disabled
                secret: whsec_EXAMPLE00000000000000000
                last_status: null
                last_delivery_at: null
                failure_streak: 0
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
      x-audience: public
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - recovery.opened
                      - recovery.recovered
                      - recovery.lost
                      - outcome.confirmed
                      - outcome.reverted
                      - billing.retry_requested
                      - billing.cancel_requested
                      - billing.discount_requested
                      - billing.pause_requested
                      - billing.downgrade_requested
                      - customer.scored
                      - customer.flagged
                      - customer.access_restricted
                      - customer.access_restored
                      - cancel_session.started
                      - cancel_session.offer_accepted
                      - cancel_session.completed
                      - winback.reactivated
                      - "*"
                description:
                  type: string
                status:
                  type: string
                  enum:
                    - enabled
                    - disabled
                rotate_secret:
                  type: boolean
                  description: The new secret is returned once.
            example:
              rotate_secret: true
              status: disabled
        required: true
    delete:
      summary: Remove an endpoint
      tags:
        - Developers
      operationId: deleteWebhookEndpoint
      responses:
        "204":
          description: removed
      x-audience: public
  /v1/webhook_deliveries/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    get:
      summary: One delivery, with its payload
      tags:
        - Developers
      operationId: getWebhookDelivery
      responses:
        "200":
          description: found
          content:
            application/json:
              example:
                object: webhook_delivery
                id: whd_FySimzUDNEgANYU3
                livemode: false
                endpoint: whk_1xbKsAAZvYm1Xmn7
                destination: http
                event: nevt_1
                type: recovery.opened
                status: failed
                attempts: 7
                next_attempt_at: null
                response_status: null
                response_ms: null
                error: null
                delivered_at: null
                failed_at: null
                replay_of: null
                created_at: 2026-11-04T10:00:00Z
                payload:
                  id: nevt_1
                  type: recovery.opened
                response_excerpt: null
              schema:
                $ref: "#/components/schemas/WebhookDeliveryDetail"
      x-audience: public
  /v1/webhook_deliveries/{id}/replay:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    post:
      summary: Replay a delivery (same payload, same event id)
      tags:
        - Developers
      operationId: replayWebhookDelivery
      responses:
        "201":
          description: sent again now
          content:
            application/json:
              example:
                object: webhook_delivery
                id: whd_QgnnVzREeDX8CRSm
                livemode: false
                endpoint: whk_pbUg1sc39Ttpip9b
                destination: http
                event: nevt_1
                type: recovery.opened
                status: delivered
                attempts: 1
                next_attempt_at: null
                response_status: 200
                response_ms: 42
                error: null
                delivered_at: 2026-11-04T10:00:00Z
                failed_at: null
                replay_of: whd_63HJxMXhoVWNsVmW
                created_at: 2026-11-04T10:00:00Z
                payload:
                  id: nevt_1
                response_excerpt: null
              schema:
                $ref: "#/components/schemas/WebhookDeliveryDetail"
      x-audience: public
  /v1/cancel_sessions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-NoChurn-Customer-Token
        in: header
        required: false
        schema:
          type: string
      - name: Origin
        in: header
        required: false
        schema:
          type: string
    get:
      summary: "Read a cancel session (widget: reload after an error)"
      tags:
        - Cancel flow
      operationId: getCancelSession
      description: Publishable key + the customer token the session was opened with + allowed Origin, or a secret key. cancel_url_now is only given when the session is created.
      responses:
        "200":
          description: the session at its current step
          content:
            application/json:
              example:
                object: cancel_session
                id: ses_cXpnGr9TadrEogEp
                livemode: false
                step: reason
                outcome: null
                locale: en
                holdout: false
                published: true
                customer:
                  id: cst_czsHTg8pzuqqXMB7
                  stripe_customer_id: cus_1
                  external_id: user_42
                subscription:
                  id: nsub_FDNQxWAvM9FaEmV7
                  plan: null
                  status: active
                  amount_cents: 4900
                  currency: eur
                  interval: month
                  interval_count: 1
                  current_period_end: 2026-11-24T10:00:00Z
                  cancel_at_period_end: false
                subscriptions: []
                reasons:
                  - key: too_expensive
                    label: It's too expensive
                    free_text: false
                  - key: missing_feature
                    label: A feature is missing
                    free_text: false
                  - key: not_using
                    label: I don't use it enough
                    free_text: false
                  - key: switching
                    label: I'm switching to another tool
                    free_text: false
                  - key: temporary
                    label: I only need a break
                    free_text: false
                  - key: project_finished
                    label: My project is finished
                    free_text: false
                  - key: other
                    label: Something else
                    free_text: true
                offer: null
                talk_url: null
                texts:
                  review_ask: Would you leave a short review on {{platform}}? No reward, just your honest opinion.
                  review_cta: Leave a review
                  title: Before you go
                  reason_prompt: Why are you cancelling?
                  other_placeholder: Tell us more (optional)
                  offer_title: One last thing
                  accept: Accept the offer
                  decline: No thanks, continue
                  feedback_prompt: Anything we could have done better?
                  feedback_submit: Continue
                  confirm_title: Cancel your subscription?
                  confirm_body: You keep access until the end of the paid period.
                  cancel_now: Cancel my subscription
                  saved_title: Thanks for staying!
                  cancelled_title: Your subscription is cancelled
                  cancelled_body: You keep access until the end of the paid period.
                  offer_discount: "{{percent}}% off for {{months}} months"
                  offer_pause: Pause for {{months}} months, free
                  offer_downgrade: Switch to {{plan}}
                  offer_talk: Talk to the team
                  choose_subscription_title: Which subscription?
                  choose_subscription_body: You have several subscriptions. Choose the one you want to cancel.
                  choose_subscription_select: Continue with this one
                  cancel_this_one: Cancel this one
                  renews_on: Renews on {{date}}
                feedback_enabled: true
                feedback:
                  enabled: true
                  required: false
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                cancel_url_now: null
                cancel_status: null
                cancel_effective_at: null
                result: null
                access_until: null
                confirmation_email_sent: false
                review: null
                expires_at: 2026-11-05T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/CancelSession"
        "404":
          description: a session of another customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
  /v1/cancel_sessions/{id}/answer:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-NoChurn-Customer-Token
        in: header
        required: false
        schema:
          type: string
      - name: Origin
        in: header
        required: false
        schema:
          type: string
    post:
      summary: Answer the current step
      tags:
        - Cancel flow
      operationId: answerCancelSession
      description: "One body per step: choose_subscription → { subscription } (one of the session's subscriptions, DECISIONS 72); reason → { reason, comment? } (comment for a free-text reason); offer → { accept_offer }; feedback → { feedback? } (required when feedback.required). Cancelling never needs an answer: POST /cancel."
      parameters: []
      responses:
        "200":
          description: the next step, with the offer in the account's words
          content:
            application/json:
              example:
                object: cancel_session
                id: ses_gXwed1ERtHMt3LtB
                livemode: false
                step: offer
                outcome: null
                locale: fr
                holdout: false
                published: true
                customer:
                  id: cst_SS392NRTjKfXKLRS
                  stripe_customer_id: cus_1
                  external_id: user_42
                subscription:
                  id: nsub_UXXWSnvbbahnokAE
                  plan: null
                  status: active
                  amount_cents: 4900
                  currency: eur
                  interval: month
                  interval_count: 1
                  current_period_end: 2026-11-24T10:00:00Z
                  cancel_at_period_end: false
                subscriptions: []
                reasons:
                  - key: too_expensive
                    label: C'est trop cher
                    free_text: false
                  - key: missing_feature
                    label: Il manque une fonctionnalité
                    free_text: false
                  - key: not_using
                    label: Je ne l'utilise pas assez
                    free_text: false
                  - key: switching
                    label: Je passe à un autre outil
                    free_text: false
                  - key: temporary
                    label: J'ai juste besoin d'une pause
                    free_text: false
                  - key: project_finished
                    label: Mon projet est terminé
                    free_text: false
                  - key: other
                    label: Autre chose
                    free_text: true
                offer:
                  id: off_A8q4hT3LvQy4tvDX
                  kind: discount
                  title: "30 % de moins, 3 mois : restez !"
                  url: null
                  percent: 30
                  months: 3
                  plan_name: null
                talk_url: null
                texts:
                  review_ask: Laisseriez-vous un court avis sur {{platform}} ? Aucune récompense, juste votre avis sincère.
                  review_cta: Laisser un avis
                  title: Avant de partir
                  reason_prompt: Pourquoi résiliez-vous ?
                  other_placeholder: Dites-nous en plus (facultatif)
                  offer_title: Une dernière chose
                  accept: Accepter l'offre
                  decline: Non merci, continuer
                  feedback_prompt: Qu'aurions-nous pu faire mieux ?
                  feedback_submit: Continuer
                  confirm_title: Résilier votre abonnement ?
                  confirm_body: Vous gardez l'accès jusqu'à la fin de la période payée.
                  cancel_now: Résilier mon abonnement
                  saved_title: Merci de rester !
                  cancelled_title: Votre abonnement est résilié
                  cancelled_body: Vous gardez l'accès jusqu'à la fin de la période payée.
                  offer_discount: "{{percent}} % de moins, {{months}} mois : restez !"
                  offer_pause: Mettre en pause {{months}} mois, gratuitement
                  offer_downgrade: Passer à {{plan}}
                  offer_talk: Parler à l'équipe
                  choose_subscription_title: Quel abonnement ?
                  choose_subscription_body: Vous avez plusieurs abonnements. Choisissez celui que vous voulez résilier.
                  choose_subscription_select: Continuer avec celui-ci
                  cancel_this_one: Résilier celui-ci
                  renews_on: Renouvellement le {{date}}
                feedback_enabled: true
                feedback:
                  enabled: true
                  required: false
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                cancel_url_now: null
                cancel_status: null
                cancel_effective_at: null
                result: null
                access_until: null
                confirmation_email_sent: false
                review: null
                expires_at: 2026-11-05T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/CancelSession"
        "400":
          description: a required feedback left empty
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: "choose_subscription: not one of the customer's live subscriptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subscription:
                  type: string
                  example: nsub_7Hd2kQ9xLm3VbN4p
                  description: "choose_subscription step: one of the customer's live subscriptions (nsub_…, sub_… or its id in your billing)."
                reason:
                  type: string
                  example: too_expensive
                  description: "Reason step: one of the session's reasons[].key."
                comment:
                  type: string
                  maxLength: 2000
                  description: "Reason step: free text (reasons with free_text)."
                accept_offer:
                  type: boolean
                  description: "Offer step: true keeps the subscription with the offer (talk: talk_url, the session goes on)."
                feedback:
                  type: string
                  maxLength: 2000
                  description: "Feedback step: optional unless feedback.required."
            example:
              reason: too_expensive
        required: true
  /v1/cancel_sessions/{id}/cancel:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - name: X-NoChurn-Customer-Token
        in: header
        required: false
        schema:
          type: string
      - name: Origin
        in: header
        required: false
        schema:
          type: string
    post:
      summary: Cancel now (click to cancel, at any step)
      tags:
        - Cancel flow
      operationId: cancelCancelSession
      description: "Works at every step, after expiry, for the holdout, an unpublished flow and an unknown customer. Idempotent: a retry never cancels twice. cancel_status: done (Stripe), requested (your billing, billing.cancel_requested), pending (NoChurn retries until it is done). At choose_subscription (several live subscriptions, none chosen) the body names the one to cancel; without it, 422 subscription_required: never a guess, never all of them (DECISIONS 72)."
      parameters: []
      responses:
        "200":
          description: cancelled
          content:
            application/json:
              example:
                object: cancel_session
                id: ses_GMLvwQHvtdEcp5DZ
                livemode: false
                step: done
                outcome: cancelled
                locale: en
                holdout: false
                published: true
                customer:
                  id: cst_Jy3PTUaA6DX1CQc3
                  stripe_customer_id: cus_1
                  external_id: user_42
                subscription:
                  id: nsub_MstpgaamkomcxhiZ
                  plan: null
                  status: active
                  amount_cents: 4900
                  currency: eur
                  interval: month
                  interval_count: 1
                  current_period_end: 2026-11-24T10:00:00Z
                  cancel_at_period_end: true
                subscriptions: []
                reasons:
                  - key: too_expensive
                    label: It's too expensive
                    free_text: false
                  - key: missing_feature
                    label: A feature is missing
                    free_text: false
                  - key: not_using
                    label: I don't use it enough
                    free_text: false
                  - key: switching
                    label: I'm switching to another tool
                    free_text: false
                  - key: temporary
                    label: I only need a break
                    free_text: false
                  - key: project_finished
                    label: My project is finished
                    free_text: false
                  - key: other
                    label: Something else
                    free_text: true
                offer: null
                talk_url: null
                texts:
                  review_ask: Would you leave a short review on {{platform}}? No reward, just your honest opinion.
                  review_cta: Leave a review
                  title: Before you go
                  reason_prompt: Why are you cancelling?
                  other_placeholder: Tell us more (optional)
                  offer_title: One last thing
                  accept: Accept the offer
                  decline: No thanks, continue
                  feedback_prompt: Anything we could have done better?
                  feedback_submit: Continue
                  confirm_title: Cancel your subscription?
                  confirm_body: You keep access until the end of the paid period.
                  cancel_now: Cancel my subscription
                  saved_title: Thanks for staying!
                  cancelled_title: Your subscription is cancelled
                  cancelled_body: You keep access until the end of the paid period.
                  offer_discount: "{{percent}}% off for {{months}} months"
                  offer_pause: Pause for {{months}} months, free
                  offer_downgrade: Switch to {{plan}}
                  offer_talk: Talk to the team
                  choose_subscription_title: Which subscription?
                  choose_subscription_body: You have several subscriptions. Choose the one you want to cancel.
                  choose_subscription_select: Continue with this one
                  cancel_this_one: Cancel this one
                  renews_on: Renews on {{date}}
                feedback_enabled: true
                feedback:
                  enabled: true
                  required: false
                theme:
                  object: cancel_flow_theme
                  preset: light
                  format: modal
                  tokens:
                    bg: "#f5f6fa"
                    surface: "#ffffff"
                    text: "#1c1e4a"
                    muted: "#686b85"
                    accent: "#4637d9"
                    accent_text: "#ffffff"
                    border: "#e3e5ec"
                    danger: "#c2410c"
                    font: system-ui, sans-serif
                    font_size: 15
                    radius: 12
                    radius_control: 8
                    overlay: 0.5
                    max_width: 480
                    shadow: md
                    spacing: regular
                    border_width: 1
                    border_style: solid
                    font_scale: 1.25
                    font_weight_title: 600
                  font_url: null
                  brand: logo
                  logo_url: null
                  app_name: Acme
                  end_screen:
                    style: celebrate
                    auto_close_seconds: 6
                  custom_css: ""
                  cancel_button_check: ok
                  classes:
                    - nc-modal
                    - nc-title
                    - nc-reason
                    - nc-offer
                    - nc-btn-primary
                    - nc-btn-cancel
                    - nc-footer
                cancel_url_now: null
                cancel_status: done
                cancel_effective_at: 2026-11-24T10:00:00Z
                result: null
                access_until: 2026-11-24T10:00:00Z
                confirmation_email_sent: false
                review: null
                expires_at: 2026-11-05T10:00:00Z
                created_at: 2026-11-04T10:00:00Z
              schema:
                $ref: "#/components/schemas/CancelSession"
        "422":
          description: several live subscriptions and none named
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                subscription:
                  type: string
                  description: "choose_subscription step: the one to cancel (nsub_…, sub_… or its id in your billing)."
            example: {}
  /v1/cancel_flows/{id}/theme:
    parameters:
      - name: id
        in: path
        description: default or flw_…
        required: true
        schema:
          type: string
    get:
      summary: The cancel modal's design (tokens, format, logo, CSS)
      tags:
        - Cancel flow
      operationId: getCancelFlowTheme
      description: The published version of the key's mode. Every token is present (preset, then the account's). For native apps and the widget; also inside each cancel session (with its segment / A/B design).
      responses:
        "200":
          description: the theme
          content:
            application/json:
              example:
                object: cancel_flow_theme
                preset: dark
                format: panel
                tokens:
                  bg: "#050505"
                  surface: "#0b0b0c"
                  text: "#ededed"
                  muted: "#9b9ba1"
                  accent: "#ededed"
                  accent_text: "#050505"
                  border: "#2a2a2e"
                  danger: "#f97316"
                  font: Inter, sans-serif
                  font_size: 15
                  radius: 12
                  radius_control: 8
                  overlay: 0.6
                  max_width: 480
                  shadow: none
                  spacing: compact
                  border_width: 2
                  border_style: dashed
                  font_scale: 1.5
                  font_weight_title: 500
                font_url: https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap
                brand: logo
                logo_url: https://cdn.acme.com/logo.svg
                app_name: Acme
                end_screen:
                  style: celebrate
                  auto_close_seconds: 6
                custom_css: ""
                cancel_button_check: ok
                classes:
                  - nc-modal
                  - nc-title
                  - nc-reason
                  - nc-offer
                  - nc-btn-primary
                  - nc-btn-cancel
                  - nc-footer
              schema:
                $ref: "#/components/schemas/CancelFlowTheme"
      x-audience: browser
  /v1/widget/ping:
    post:
      summary: "The widget loaded (onboarding: detected)"
      tags:
        - Widget
      operationId: pingWidget
      parameters:
        - name: Origin
          in: header
          required: false
          schema:
            type: string
      description: Publishable key + allowed Origin, no customer token. The first ping marks the widget as detected (Event widget.detected). Returns the key's fallback cancel link (DECISIONS 71).
      responses:
        "200":
          description: detected
          content:
            application/json:
              example:
                object: widget_ping
                detected_at: 2026-11-04T10:00:00Z
                cancel_fallback_url: http://localhost:3000/k/pk_test_cuzA5wx4ZZPE357jiSJReKBsj1iaeavi
              schema:
                $ref: "#/components/schemas/WidgetPing"
      x-audience: browser
  /v1/widget/guard_report:
    post:
      summary: The widget had to restore the hidden cancel button
      tags:
        - Widget
      operationId: reportWidgetGuard
      description: "Sent by the modal when its render-time check (click to cancel) found the cancel button hidden and repaired it. Publishable key only + customer token + allowed Origin. Same alert as the server-side CSS check: Event cancel_flow.css_fallback_applied (data.source widget), at most one per flow and mode per hour, and a To do item until the next publish. Limits: 10 reports a minute per key and customer, 60 per IP (429)."
      parameters:
        - name: X-NoChurn-Customer-Token
          in: header
          required: false
          schema:
            type: string
        - name: Origin
          in: header
          required: false
          schema:
            type: string
      responses:
        "202":
          description: recorded once an hour
          content:
            application/json:
              example:
                object: widget_guard_report
                recorded: true
                event: nevt_aMee2SMCB7o7TYT8
              schema:
                $ref: "#/components/schemas/WidgetGuardReport"
        "400":
          description: unknown problems
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: no customer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: a secret key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - problems
              properties:
                problems:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                      - display
                      - visibility
                      - opacity
                      - pointer-events
                      - contrast
                      - size
                      - font-size
                      - covered
                  description: What the guard repaired.
                session:
                  type: string
                  description: The cancel session shown (ses_…), if any.
                css_from_code:
                  type: boolean
                  description: A theme or css was passed in code (NoChurn.cancel({ css })).
            example:
              problems:
                - display
                - contrast
              session: ses_jiTb8PYHFcUhWCni
              css_from_code: true
        required: true
  /v1/widget/events:
    post:
      summary: Track usage events from the browser (page_view, NoChurn.track)
      tags:
        - Widget
      operationId: createWidgetEvents
      description: "Called by js.nochurn.sh/v1 for your logged-in end user: page_view (path only, never the query string) and NoChurn.track(name, props). Publishable key only, in the Authorization header or as `key` in the body (navigator.sendBeacon cannot set headers); a secret key is refused (403 publishable_key_required). The customer is read from the signed widget token (`token`, HS256 { kind, value, exp }), never from the events. The body is JSON, as application/json or text/plain (a beacon, no preflight), 256 KB at most. 50 events per call. name: 1 to 64 of a-z 0-9 _ . : -; properties: flat, 20 keys, strings of 256 characters, numbers, booleans or null, 4 KB; timestamp: ISO 8601, clamped to the last 24 hours (the browser clock is not trusted); idempotency_key required (64 characters), deduplicated per mode. Errors are per event (rejected); an unknown customer rejects every event with customer_not_found. Limits: 120 events a minute per key and customer, 300 requests a minute per IP (429 rate_limited, retry_after). Neither the IP nor the user agent is stored."
      parameters:
        - name: Origin
          in: header
          required: false
          schema:
            type: string
      responses:
        "202":
          description: "a beacon: text/plain body, key in the body, no Authorization header"
          content:
            application/json:
              example:
                object: widget_event_batch
                accepted: 1
                duplicates: 0
                rejected:
                  - index: 1
                    code: invalid_request
                    message: name "Export Used" is not snake_case.
                  - index: 2
                    code: invalid_request
                    message: 'properties: "nested" is not a string, number, boolean or null.'
                  - index: 3
                    code: invalid_request
                    message: idempotency_key is required (64 characters at most).
              schema:
                $ref: "#/components/schemas/WidgetEventBatch"
        "400":
          description: a body that is not JSON
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: no widget token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: a secret key (never in a web page)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: over 120 events a minute for this key and customer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
      x-audience: browser
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - events
              properties:
                key:
                  type: string
                  description: Your publishable key, when the Authorization header cannot be sent (sendBeacon).
                  example: pk_test_…
                token:
                  type: string
                  description: The widget token signed by your server (HS256 { kind, value, exp }), as for NoChurn.init.
                client:
                  type: string
                  example: widget/1.0.0
                  description: "The script's version, when the NoChurn-Client header cannot be sent (sendBeacon): GET /v1/account/clients."
                events:
                  type: array
                  maxItems: 50
                  items:
                    type: object
                    required:
                      - name
                      - idempotency_key
                    properties:
                      name:
                        type: string
                        pattern: ^[a-z0-9_.:-]{1,64}$
                        example: page_view
                      properties:
                        type: object
                        maxProperties: 20
                        description: "Flat: strings (256), numbers, booleans or null; 20 keys, 4 KB."
                        additionalProperties:
                          nullable: true
                          oneOf:
                            - type: string
                              maxLength: 256
                            - type: number
                            - type: boolean
                      timestamp:
                        type: string
                        format: date-time
                        description: Clamped to [now - 24 h, now].
                      idempotency_key:
                        type: string
                        maxLength: 64
            example:
              token: EXAMPLE00000000000000000
              events:
                - name: page_view
                  properties:
                    path: /billing
                  timestamp: 2026-11-01T10:00:00Z
                  idempotency_key: e1
                - name: Export Used
                  idempotency_key: e2
                - name: export:csv
                  properties:
                    rows: 12
                    nested:
                      a: 1
                  idempotency_key: e3
                - name: login
          text/plain:
            schema:
              type: object
              required:
                - token
                - events
              properties:
                key:
                  type: string
                  description: Your publishable key, when the Authorization header cannot be sent (sendBeacon).
                  example: pk_test_…
                token:
                  type: string
                  description: The widget token signed by your server (HS256 { kind, value, exp }), as for NoChurn.init.
                client:
                  type: string
                  example: widget/1.0.0
                  description: "The script's version, when the NoChurn-Client header cannot be sent (sendBeacon): GET /v1/account/clients."
                events:
                  type: array
                  maxItems: 50
                  items:
                    type: object
                    required:
                      - name
                      - idempotency_key
                    properties:
                      name:
                        type: string
                        pattern: ^[a-z0-9_.:-]{1,64}$
                        example: page_view
                      properties:
                        type: object
                        maxProperties: 20
                        description: "Flat: strings (256), numbers, booleans or null; 20 keys, 4 KB."
                        additionalProperties:
                          nullable: true
                          oneOf:
                            - type: string
                              maxLength: 256
                            - type: number
                            - type: boolean
                      timestamp:
                        type: string
                        format: date-time
                        description: Clamped to [now - 24 h, now].
                      idempotency_key:
                        type: string
                        maxLength: 64
        required: true
  /v1/account/widget_secret:
    get:
      summary: Is the widget signing secret set?
      tags:
        - Developers
      operationId: getWidgetSecret
      responses:
        "200":
          description: never shows the secret
          content:
            application/json:
              example:
                object: widget_secret
                configured: false
                created_at: null
                secret: null
              schema:
                $ref: "#/components/schemas/WidgetSecret"
      x-audience: public
    post:
      summary: Create or rotate the widget signing secret
      tags:
        - Developers
      operationId: rotateWidgetSecret
      description: Your server signs customer tokens with it (HS256). Shown once; rotating invalidates tokens signed with the old one.
      responses:
        "201":
          description: shown once
          content:
            application/json:
              example:
                object: widget_secret
                configured: true
                created_at: 2026-11-04T10:00:00Z
                secret: ncws_EXAMPLE00000000000000000
              schema:
                $ref: "#/components/schemas/WidgetSecret"
      x-audience: public
